Pulsar 1.119.0
There’s nothing here.
diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/404.html b/404.html new file mode 100644 index 0000000000..d51ab9dd0a --- /dev/null +++ b/404.html @@ -0,0 +1,223 @@ + + +
+ + + + +title: Example Post\\nauthor: Daeraxa\\ndate: 2022-11-12\\ncategory:
\\nThis is the excerpt.
\\n","headers":[],"git":{"updatedTime":1668308645000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":8}]},"readingTime":{"minutes":0.21,"words":63},"filePathRelative":"blog/20221112-Daeraxa-ExamplePost.md"}');export{e as data}; diff --git a/assets/20221127-confused-Techie-SunsetMisadventureBackend.html.181ef131.js b/assets/20221127-confused-Techie-SunsetMisadventureBackend.html.181ef131.js new file mode 100644 index 0000000000..6f8be6241d --- /dev/null +++ b/assets/20221127-confused-Techie-SunsetMisadventureBackend.html.181ef131.js @@ -0,0 +1,4 @@ +import{_ as i,o as s,c as r,e as h,a as e,b as t,d as o,f as n,r as l}from"./app.0e1565ce.js";const d={},c=e("p",null,"What's the natural response to finding out an application you use is being sunset? Spend the next several months creating a complex backend infrastructure of course!",-1),u=e("p",null,"In this blog post, which is my first so apologies if the style is widely inconsistent with everything else, I hope to get a place to talk about the journey that's occurred after the announcement of Atom's Sunset and the larger-than-assumed effect it has had on my life.",-1),m={href:"https://github.blog/2022-06-08-sunsetting-atom/",target:"_blank",rel:"noopener noreferrer"},p=e("p",null,"Now to give some background on where I was, I've never actually looked at the Atom source code. I used it every day, was the first thing I installed on new computers. But I liked it as a tool, and never paid much mind to how it was internally structured. But one of my favourite things was packages. The ability to install a package that can do nearly anything has always been amazing to me. I know many other applications can do this, but with this one feature Atom was everything I had ever wanted out of a text editor, and more so could be anything I ever want in the future.",-1),g=e("p",null,"So I knew with the announcement that this would mean I wouldn't be able to install packages. So right away I did what anyone would, find out how to keep packages going.",-1),w=e("h3",{id:"the-archival",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#the-archival","aria-hidden":"true"},"#"),t(" The Archival")],-1),f={href:"https://github.com/confused-Techie/AtomPackagesArchive",target:"_blank",rel:"noopener noreferrer"},b={href:"https://github.com/atom-community",target:"_blank",rel:"noopener noreferrer"},y=e("p",null,"Now I'll skip over the details about Atom Community, and the eventual split that occurred and focus on what my role was in the programming aspect.",-1),k=e("code",null,"+4 | -1",-1),v={href:"https://github.com/confused-Techie/AtomPackagesArchive/pull/1",target:"_blank",rel:"noopener noreferrer"},_=e("code",null,"confused-Techie/AtomPackagesArchive",-1),I=e("h3",{id:"what-to-do-with-this-data",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#what-to-do-with-this-data","aria-hidden":"true"},"#"),t(" What to do with this Data")],-1),A=e("p",null,"So now armed with a few thousand JSON files of Atom's Archived data, I had to figure out what made their backend tick. And since the Atom Package Repository is seemingly the only part of Atom that wasn't open source, I had nothing to base this on at all. So I just started hammering their servers.",-1),T=e("p",null,"Hitting every single endpoint that was in the documentation with each variance of query parameters and HTTP Methods that I could think of. To see what broke, how it broke, and what it says when it breaks.",-1),S={href:"https://github.com/confused-Techie/atom-community-server-backend",target:"_blank",rel:"noopener noreferrer"},N=e("code",null,"atom-community-server-backend",-1),x=e("code",null,"GET atom.io/api/packages/:invalidName",-1),B=e("code",null,'{ message: "Not Found" }',-1),P=e("em",null,"but",-1),O=e("code",null,"GET atom.io/api/users/:invalidName/stars",-1),j=e("code",null,'{ message: "Not found" }',-1),R=e("code",null,"confused-Techie/atom-community-server-backend-JS",-1),G={href:"https://github.com/confused-Techie/atom-backend",target:"_blank",rel:"noopener noreferrer"},J=e("code",null,"confused-Techie/atom-backend",-1),H=e("h3",{id:"the-misadventures-part",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#the-misadventures-part","aria-hidden":"true"},"#"),t(" The Misadventures Part")],-1),q=e("p",null,"At this point speed was picking up, there was so much to do and so little time. Rapid fire code was being written and things were being tested.",-1),z=e("p",null,"One thing that needed to be done was to take this mess of data I had archived and check it all. The most important thing I wanted to do, checked if everything was still valid, that is see if the GitHub repo each package pointed to still existed.",-1),C=e("p",null,"Now in doing this, I ended up needing to look up a repo on GitHub many times. So many times I accidentally got my IP blocked from them. A huge oversight on my part, and not at all an issue with them, but I remember the stress that was caused when attempting to work together with several others on GitHub while being blocked by GitHub.",-1),E={href:"https://github.com/confused-Techie/atom-package-migrator",target:"_blank",rel:"noopener noreferrer"},L=e("code",null,"confused-Techie/atom-package-migrator",-1),M=n("Now this of itself was a feat, considering the following:
But from there we now had 11,074 packages that we needed to keep safe and find a way to distribute to any user of the new Atom, wherever that ended up being.
At this point, the other hardest part was that the Atom website for lack of better words, was on its last legs. With constant errors, crashes, and generally just failing to respond, it was clear things were nearing an end. Even worse was that when it would work, it was inundated with thousands of Spam packages. In the days after my initial archive, the original Package Repository had ballooned with spam packages now with their total packages totaling in... well. I just can't quite say. Even now during the time of writing I've spent ten minutes refreshing Atom.io/packages
and it won't stop timing out or returning 500 errors. But for the sake of my point, I've kept trying, to confirm that the original Package Repository has ballooned with spam packages now to contain 405,137 Packages. But remember when I did my initial archival there were only 12,470. Just to convey how much spam this is, and how ridiculous it is that the service turned into this immediately after the announcement.
But the last hardest part of this stage was the problems that I created for myself.
Now you see, up until this point I've never had to worry about handling this much data. It wasn't until it was too late did I find out that my methodology was ill-fitting. I had written a nearly complete feature parity Backend for the Atom Package Repository, that expected all of its data to be JSON. Now anyone that's done this before will say that was a dumb choice, but forgive me for only being smaller scale prior. The excitement of being nearly done immediately faded, when I got the new backend running in the cloud, and threw all of my data at it. Every single package.
",6),W=e("code",null,"/api/packages",-1),D={href:"https://github.com/confused-Techie/atom-backend/commit/f792a4975f932f20528b871e189300bd97585dac",target:"_blank",rel:"noopener noreferrer"},V=e("code",null,"f792a4975f932f20528b871e189300bd97585dac",-1),Q=n(`// One note of concern with chaining all of these together, is that this will potentially loop
+// through the entire array of packages 3 times, resulting in a
+// linear time complexity of O(3). But testing will have to determine how much that is a factor of concern.
+
Now I'm no mathematician and my understanding of Time Complexity is best described as weak, but I knew it could be a bad idea. And boy was it.
Resuming after I had pushed everything to the cloud and hit my endpoint, I got no response. Or rather I thought it was no response. The page took well over 300
seconds before most often it would time out.
So this meant I had to rewrite the entire thing to no longer assume we had JSON data, that's when we had to migrate the entire Backend Codebase to use an SQL server.
Once the rewrite was done I posted the following on our Discord server.
But heres some news I'm very excited to see, about the improvements of performance on the iterations of the backend, when querying /api/packages
(As in get all packages with default sorts)
Keep in mind these times are from the server itself. So that includes the express server getting the request, filtering query parameters, querying the SQL server over the web, and rebuilding/pruning the JSON response. But this is great to see, and is finally something that seems shippable, even if it can be further optimized
So with that, it was time to finish rewriting the backend, which while the struggles were not over, we had at least gotten over the (as so far) biggest hurdle.
But beyond this, it was at this point we had a proper community. A team of skilled contributors are all willing to pull their weight to make this happen. We all have lives outside this project, we all live in different areas, in different timezones, and speak different languages. But the one thing we have in common is our mission, of keeping an Open Source Project we care about alive.
Throughout all of this, that has been my biggest takeaway. The most important lesson I think is available to learn here, and one I don't want to forget any time soon.
What's the natural response to finding out an application you use is being sunset?
Take pride in the Open-Source community around you, and be thankful we live in a time where all that's needed to build a community and team is the text editor you choose to use.
Thanks for reading, and as I always say thanks for contributing.
`,18);function U(F,K){const a=l("ExternalLinkIcon");return s(),r("div",null,[c,h(" more "),u,e("p",null,[t("Like some others, I'm assuming, on a normal day I was browsing Reddit when I saw the initial announcement. On June 8th of 2022 GitHub officially "),e("a",m,[t("announced"),o(a)]),t(" that they were going to sunset Atom.")]),p,g,w,e("p",null,[t("On June 12th of 2022, I wrote a "),e("a",f,[t("super simple script"),o(a)]),t(", one that would just absolutely hammer Atom's package repository and get everything I possibly could. The only goal here was to archive every single package and all of their versions. Hoping that later I'd figure out what I could do with this data.")]),e("p",null,[t("It was around that same time I started to see "),e("a",b,[t("Atom Community"),o(a)]),t(" mentioned as the most likely fork to succeed after Atom. So of course seeing their GitHub I joined their Discord and got going with a group of other talented volunteers to start seeing what needed to be done.")]),y,e("p",null,[t("After creating this small script, I think what really made all of this feel like an actual team effort was the first PR I received for this project. A small diff, only "),k,t(" over on "),e("a",v,[_,o(a)]),t(" by @DeeDeeG, but what it really meant was that truly there was a community here that cared about keeping this whole ecosystem alive the same way I did. And from there, that's all that was needed to be off to the races.")]),I,A,T,e("p",null,[t("On June 13th of 2022, I started the "),e("a",S,[N,o(a)]),t(", what I thought would be the home of the new Backend for Atom Community, and there I delved into the strange idiosyncrasies of Atom. Still, the strangest one that sticks in my head, is I could tell how strange the codebase was just from the error messages. The best example, "),x,t(" Returns "),B,t(),P,t(" hitting "),O,t(" returns "),j,t(". Meaning that there was completely different code handling not found in one place, and in another. Notice the difference in capitalization.")]),e("p",null,[t("But soon after creating the repo, I realized that no one else on this small team we had was familiar with Golang at all, which is what I had initially written this in. So I switched over to JavaScript, which in the long run was likely a great choice as that is my most familiar language. And that's where I created "),R,t(", a mouthful I know, but was much later renamed just to "),e("a",G,[J,o(a)]),t(".")]),H,q,z,C,e("p",null,[t("In the end, the "),e("a",E,[L,o(a)]),t(" was able to check if every single package was valid, while also removing a list of banned package names that we had put together.")]),M,e("p",null,[t("Then simply I requested the default screen "),W,t(" which, in my poor implementation would scan every single package, then would filter it based on query parameters. Which would be iterating through every single one of them. Now you may hear this and think of course performance was terrible, and trust me it was a concern I had to, on commit "),e("a",D,[V,o(a)]),t(" I wrote the following comment after creating this terrible method.")]),Q])}const Y=i(d,[["render",U],["__file","20221127-confused-Techie-SunsetMisadventureBackend.html.vue"]]);export{Y as default}; diff --git a/assets/20221127-confused-Techie-SunsetMisadventureBackend.html.dad96414.js b/assets/20221127-confused-Techie-SunsetMisadventureBackend.html.dad96414.js new file mode 100644 index 0000000000..15d5250af9 --- /dev/null +++ b/assets/20221127-confused-Techie-SunsetMisadventureBackend.html.dad96414.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-0075970e","path":"/blog/20221127-confused-Techie-SunsetMisadventureBackend.html","title":"A Sunset and the Misadventures of a Backend","lang":"en-US","frontmatter":{"title":"A Sunset and the Misadventures of a Backend","author":"confused-Techie","date":"2022-11-27T00:00:00.000Z","category":["dev","log"],"tag":["backend","sunset"]},"excerpt":"What's the natural response to finding out an application you use is being sunset?\\nSpend the next several months creating a complex backend infrastructure of course!
\\n","headers":[{"level":3,"title":"The Archival","slug":"the-archival","link":"#the-archival","children":[]},{"level":3,"title":"What to do with this Data","slug":"what-to-do-with-this-data","link":"#what-to-do-with-this-data","children":[]},{"level":3,"title":"The Misadventures Part","slug":"the-misadventures-part","link":"#the-misadventures-part","children":[]},{"level":3,"title":"The Good News","slug":"the-good-news","link":"#the-good-news","children":[]}],"git":{"updatedTime":1669648250000,"contributors":[{"name":"confused_techie","email":"dev@lhbasics.com","commits":2},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":6.49,"words":1948},"filePathRelative":"blog/20221127-confused-Techie-SunsetMisadventureBackend.md","localizedDate":"November 27, 2022"}`);export{e as data}; diff --git a/assets/20221208-Daeraxa-DistroTubeVideo.html.7c68eb7c.js b/assets/20221208-Daeraxa-DistroTubeVideo.html.7c68eb7c.js new file mode 100644 index 0000000000..0e6a6047e1 --- /dev/null +++ b/assets/20221208-Daeraxa-DistroTubeVideo.html.7c68eb7c.js @@ -0,0 +1 @@ +import{_ as n,o as a,c,a as e,b as t,d as r,e as s,r as l}from"./app.0e1565ce.js";const u={},i=e("br",null,null,-1),h={href:"https://www.youtube.com/watch?v=WA1c_S6Zsu4",target:"_blank",rel:"noopener noreferrer"},d=e("p",null,"DistoTube is a channel/website all about free and open source software and one of our members reached out to tell them about the Pulsar project and he decided to feature us in an entire video!",-1),_={href:"https://www.youtube.com/watch?v=WA1c_S6Zsu4",target:"_blank",rel:"noopener noreferrer"},p=e("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/WA1c_S6Zsu4",title:"YouTube video player",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1),f={href:"https://odysee.com/@DistroTube:2/announcing-the-pulsar-text-editor:7",target:"_blank",rel:"noopener noreferrer"},m=e("p",null,"And if you see this DT, thank you very much for the feature, we will be taking your comments to heart!",-1);function b(w,y){const o=l("ExternalLinkIcon");return a(),c("div",null,[e("p",null,[t("Today we were featured on the DistroTube YouTube channel!"),i,e("a",h,[t("Watch here"),r(o)])]),s(" more "),d,e("p",null,[t("If you haven't seen it then "),e("a",_,[t("click here"),r(o)]),t(" or watch below:")]),p,e("p",null,[t("Or if you prefer you can watch it "),e("a",f,[t("on Odysee"),r(o)])]),m])}const T=n(u,[["render",b],["__file","20221208-Daeraxa-DistroTubeVideo.html.vue"]]);export{T as default}; diff --git a/assets/20221208-Daeraxa-DistroTubeVideo.html.a94d35bc.js b/assets/20221208-Daeraxa-DistroTubeVideo.html.a94d35bc.js new file mode 100644 index 0000000000..40a154739e --- /dev/null +++ b/assets/20221208-Daeraxa-DistroTubeVideo.html.a94d35bc.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-cfc0df38","path":"/blog/20221208-Daeraxa-DistroTubeVideo.html","title":"Featured on DistroTube!","lang":"en-US","frontmatter":{"title":"Featured on DistroTube!","author":"Daeraxa","date":"2022-12-08T00:00:00.000Z","category":["news"],"tag":["video"]},"excerpt":"Today we were featured on the DistroTube YouTube channel!
\\nWatch here
Over all, we hope you enjoy the release, take a look, kick the tires, let us know what you think. Report any issues you find, and maybe we'll be able to put together a fix. Likewise, if you have a fix handy, feel free to open a Pull Request. Happy coding, see you among the stars!
github
Package v0.36.13Check out our first GitHub Release for Pulsar! Available Now!
Check out our Second GitHub Release for Pulsar! Available Now!
With the release of Pulsar Beta 1.101.0 we have quite a number of improvements and fixes to various issues that have been identified and logged by our wonderful community.
Have a read of the changelog for the full details but some highlights include fixes to the Pulsar logo on Windows and Linux, a bunch of macOS changes to bring back some of the original functionality with opening files, and the resolution of an annoying issue on Linux that could stop the app from launching at all!
Thank you so much to everyone who has been involved in this release, every comment to our discussions board or Discord server, every issue logged on GitHub and every pull request created have all contributed to getting us where we are today.
And a special thank you for all of the donations we have received, we have some plans to use these wisely for the good of the project so look forward to the next big release!
So please, if you have any comments then please join us on our Discord or discussions page, if you find any bugs or regressions then let us know via a GitHub issue (and if you can fix it (or anything else) then feel free to open a pull request).
And as always, happy coding, see you among the stars!
--version
, --package
or --help
did not show outputs--package
settings-view
is disabled, describing how to re-enable itWhat has the Pulsar team and community been up to lately?
\\n","headers":[{"level":2,"title":"Tree-sitter","slug":"tree-sitter","link":"#tree-sitter","children":[]},{"level":2,"title":"Package repository","slug":"package-repository","link":"#package-repository","children":[]},{"level":2,"title":"Donations and binary signing","slug":"donations-and-binary-signing","link":"#donations-and-binary-signing","children":[]},{"level":2,"title":"Matrix space","slug":"matrix-space","link":"#matrix-space","children":[]}],"git":{"updatedTime":1675286848000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":8}]},"readingTime":{"minutes":3.16,"words":948},"filePathRelative":"blog/20230201-Daeraxa-FebUpdate.md","localizedDate":"February 1, 2023"}');export{e as data}; diff --git a/assets/20230209-mauricioszabo-tree-sitter-part-1.html.580bcde1.js b/assets/20230209-mauricioszabo-tree-sitter-part-1.html.580bcde1.js new file mode 100644 index 0000000000..8f5f339f98 --- /dev/null +++ b/assets/20230209-mauricioszabo-tree-sitter-part-1.html.580bcde1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5fdcc4a1","path":"/blog/20230209-mauricioszabo-tree-sitter-part-1.html","title":"Tales of Tree-Sitter part 1: the start of a tale","lang":"en-US","frontmatter":{"title":"Tales of Tree-Sitter part 1: the start of a tale","author":"Maur\xEDcio","date":"2023-02-09T00:00:00.000Z","category":["dev"],"tag":["modernization","tree-sitter"]},"excerpt":"How did I decide to start working on tree-sitter, and all preparations to\\nmodernize it on Pulsar
\\n","headers":[{"level":2,"title":"Current Pulsar implementation","slug":"current-pulsar-implementation","link":"#current-pulsar-implementation","children":[]},{"level":2,"title":"Modernizing the current implementation","slug":"modernizing-the-current-implementation","link":"#modernizing-the-current-implementation","children":[]}],"git":{"updatedTime":1676229228000,"contributors":[{"name":"Maur\xEDcio Szabo","email":"mauricio.szabo@gmail.com","commits":2},{"name":"Maur\xEDcio Szabo","email":"mauricioszabo@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":2.96,"words":889},"filePathRelative":"blog/20230209-mauricioszabo-tree-sitter-part-1.md","localizedDate":"February 9, 2023"}');export{e as data}; diff --git a/assets/20230209-mauricioszabo-tree-sitter-part-1.html.80ed8667.js b/assets/20230209-mauricioszabo-tree-sitter-part-1.html.80ed8667.js new file mode 100644 index 0000000000..0b1f516881 --- /dev/null +++ b/assets/20230209-mauricioszabo-tree-sitter-part-1.html.80ed8667.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as a,e as i,a as o,f as r}from"./app.0e1565ce.js";const n={},s=o("p",null,"How did I decide to start working on tree-sitter, and all preparations to modernize it on Pulsar",-1),h=r('First, what is tree-sitter? Tree-sitter is basically a library that reads the source code and parses into an abstract syntax tree. That tree can then be used to find indentation rules or highlight elements like local variables, symbols, tags and everything.
So, it's an alternative for TextMate grammars, but more powerful and faster. It also supports "incremental parsing", meaning that if you have a huge file that takes some time to highlight, when you make changes to that document you only need to wait for a fraction of time to parse the contents.
The way that tree-sitter works right now is basically is the same way that it was implemented in Atom a long time ago: it basically tries to match the elements of the AST and convert that into TextMate scopes. So, supposing that it finds, for example, a left right assignment (something like a = 10
)- it will basically try to convert the "left token" to a variable.
That's where the problems begin. The new versions of tree-sitter are changing the AST in a way that Atom (or Pulsar) does not support. The second problem is this remapping: currently, it uses some pattern in CSON files, with some kind of "CSS selector"-based metalanguage. CSON is basically a YAML version of JSON and it only works in Atom (and now in Pulsar)...
Basically - it means that all this remapping only works on Pulsar, and have no other editors that supports this; which, again, means that every version bump for grammars will have to keep remapping things over and over again (considering that all tree elements and nodes are considered "internal state" of each grammar) which is far from ideal (and does not scale, specially considering that we're a small team). Finally, it means that we need a huge number of tests that are copies of Tree-Sitter and TextMate just to be sure we're doing the same job that others are doing (again, far from ideal, specially considering that programming languages evolve, and we need to keep track of all this evolution, for all languages that Pulsar supports - a work that, basically, these grammars are already doing, and will probably do better than us).
So, we would like to reuse what other editors use, and have the exact same experience without having to, from time to time, revisit all these "remappings" and that is where the queries come into action:
Modern tree-sitter uses queries, that are basically a scheme-inspired file that basically maps AST to syntax elements (for highlighting), tags elements as local variables (also to help highlighting) and basically is more powerfull than the metalanguage we use currently in Pulsar.
So recently, I was surprised with some very rare free time, and decided to work on modernizing tree-sitter on the editor. The main reason is also because, when we upgraded Pulsar to use Electron 12, we also have to update both tree-sitter and grammars, some of these are now not highlighting as they should. That, combined with the fact that the version of tree-sitter we're using (the native one, that have bindings to Node.JS) will stop working after electron 14 (that is, two versions for now) made me try to implement a WASM-based version.
And that where the problems begin.
Basically, nobody knows exactly how a "modern implementation" of tree-sitter looks like. I tried to talk with different plugin authors on different editor and they all gave me different responses. Most people rewrite the queries that come with tree-sitter, for example... questions that I asked, like for example, if there's a pattern for tokens, were answered with "if there's a pattern nobody actually learned that or even documented it"; also about queries, most people answered me that "that the query result is up to interpretation" which is not fine for my approaches...
And so, without any official guide, I decided to try to match what the tree-sitter command-line interface does. Tree-sitter has a CLI that allows to highlight files. You run with tree-sitter highlight <name-of-file>
, basically, and I found that it uses a new library that's basically not yet complete and does not have bindings for the browser (WASM) or Node. So first, I tried to analyze if it was going to be possible to expose this library to JS, but I didn't find a way to make "incremental parsing" (a huge interesting feature of tree-sitter where you basically inform to the highlighter what changed instead of the whole tree, and the library will only re-tokenize that specific fragment instead).
Then, I decided to do what some every sane person would probably not do - implement everything by hand, improvising where things didn't work. Tokenize with the CLI, and try to match the same structure in Pulsar, fixing things where that didn't work... and that's part one.
',15);function l(d,m){return t(),a("div",null,[s,i(" more "),h])}const c=e(n,[["render",l],["__file","20230209-mauricioszabo-tree-sitter-part-1.html.vue"]]);export{c as default}; diff --git a/assets/20230215-Daeraxa-v1.102.0.html.5122191c.js b/assets/20230215-Daeraxa-v1.102.0.html.5122191c.js new file mode 100644 index 0000000000..dd2f51689d --- /dev/null +++ b/assets/20230215-Daeraxa-v1.102.0.html.5122191c.js @@ -0,0 +1 @@ +import{_ as o,o as a,c as n,a as e,b as t,d as l,e as s,f as i,r as d}from"./app.0e1565ce.js";const u={},p={href:"https://github.com/pulsar-edit/pulsar/releases/tag/v1.102.0",target:"_blank",rel:"noopener noreferrer"},c=i('With the release of Pulsar 1.102.0 we have packed it full of improvements!
With a huge focus on testing this time around we hope that this will result in a more stable and functional editor in all future releases.
We've also added new icons for macOS and updated many of Pulsar and core packages dependencies.
But for the big news, our macOS releases are now signed! Meaning no fancy commands need to be run prior to installation and you get a guarantee that Pulsar is made and published by us. Also as you might've noticed, this version has no beta
in its name. We are now adopting a Rolling Release model and this can be considered the first of many stable releases. Take a look at our website for more information!
And again, special thank you to all of our wonderful community members that have helped us write code, log issues, respond to everyone, and donate to the project. We truly could not do this without each and every one of you.
And as always, happy coding, see you among the stars!
pulsar
on Windows could never triggergithub
package shelling out to git
on macOSright-clicked
CSS class on tagsmaster
for git branchesCheck out our newest Regular Release for Pulsar! Available Now!
Essentially we felt that the terminology we were using was at best, unintentionally mildly misleading and at worst, incorrect so with this change we hope to address these issues.
The previous model was as follows:
master
branch. These milestones were never formally decided upon so we instead went with a semi-arbitrary release date of the 15th of each month simply to get these releases updated with all of our recent fixes.beta
or latest
on our website to make it clear if it was a "stable" milestone release or a beta release candidate.master
branch. The idea of these were that they could be treated as dev
, alpha
or nightly
type releases which were potentially unstable but had the latest updates.However it became clear that this wasn't what we were actually doing. What ended up happening was that our "stable" releases never got a proper milestone criteria agreed upon and quickly became out of date compared to our "Cirrus CI" releases.
We also found that because all the fixes going into our master
branch were so thoroughly reviewed and tested by both the Pulsar team and wider community, these ended up being more stable than the "stable" releases which quickly meant that, whilst our intention was correct, the reality was either incorrect or misleading.
More than a few people commented on the use of the beta
tag meaning it was unfinished or unstable software (even more so with the alpha
and dev
terms used on the Cirrus builds) so were avoiding Pulsar entirely whilst they waited for a proper, stable, release which often required us to manually explain the concepts - and those are just the vocal people we heard from so we don't even know how many people got turned off from the project entirely based on this which was never our intention.
We also found that the very concept of the Cirrus CI binaries confused some people, particularly due to the name, so this also needed to be addressed.
So after some lengthy discussions and the aforementioned poll, we decided to address these issues by reflecting what is actually going on in practice.
",6),w={href:"https://pulsar-edit.dev/download.html",target:"_blank",rel:"noopener noreferrer"},g=s('The "Rolling Release" is the new name for what we previously called "Cirrus CI Binaries" and we have promoted this to the top of our downloads page to make it more obvious as we have a (soft) preference for its use - after all this is the "hyper-hackable text editor" and this seems to fit our mission statement nicely.
This type of release has a number of benefits for everyone:
Of course there are some potential drawbacks:
This is the new name for the section previously just labelled as "Releases". These have been moved down on the page but remain a valid option for those that want to use a more regular release model.
Currently we are sticking to a release date of the 15th of every month (which initially was just a happy coincidence but we found we liked it and makes for easy organisation) but this not guaranteed and may change as needed.
This also has some benefits for some over the rolling release:
v1.101.0
vs 1.101.2023021600
.Of course there are also some potential drawbacks:
We recently decided to change our release strategy to reflect what we are actually doing and address some potential inaccuracies in our existing terminology.
\\n","headers":[{"level":2,"title":"Rolling Release","slug":"rolling-release","link":"#rolling-release","children":[]},{"level":2,"title":"Regular Releases","slug":"regular-releases","link":"#regular-releases","children":[]},{"level":2,"title":"What can I expect going forwards?","slug":"what-can-i-expect-going-forwards","link":"#what-can-i-expect-going-forwards","children":[]}],"git":{"updatedTime":1679861697000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":5.32,"words":1596},"filePathRelative":"blog/20230216-Daeraxa-ReleaseStrategyUpdate.md","localizedDate":"February 16, 2023"}');export{e as data}; diff --git a/assets/20230227-Daeraxa-Survey1.html.cce17cec.js b/assets/20230227-Daeraxa-Survey1.html.cce17cec.js new file mode 100644 index 0000000000..6ebde10d55 --- /dev/null +++ b/assets/20230227-Daeraxa-Survey1.html.cce17cec.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5b193c62","path":"/blog/20230227-Daeraxa-Survey1.html","title":"Survey: How did you hear about us?","lang":"en-US","frontmatter":{"title":"Survey: How did you hear about us?","author":"Daeraxa","date":"2023-02-27T00:00:00.000Z","category":["survey"],"tag":["community","socials"]},"excerpt":"A short survey asking how you heard about the Pulsar project as well as feedback\\non our community and social presence.
\\n","headers":[],"git":{"updatedTime":1677465101000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.42,"words":125},"filePathRelative":"blog/20230227-Daeraxa-Survey1.md","localizedDate":"February 27, 2023"}');export{e as data}; diff --git a/assets/20230227-Daeraxa-Survey1.html.fb8ea37d.js b/assets/20230227-Daeraxa-Survey1.html.fb8ea37d.js new file mode 100644 index 0000000000..254b902a90 --- /dev/null +++ b/assets/20230227-Daeraxa-Survey1.html.fb8ea37d.js @@ -0,0 +1 @@ +import{_ as t,o as r,c as s,e as a,a as e,b as o,d as c,r as l}from"./app.0e1565ce.js";const i={},u=e("p",null,"A short survey asking how you heard about the Pulsar project as well as feedback on our community and social presence.",-1),d=e("p",null,"We are curious to see where people are finding out about the project and also where people are looking for news, updates and information.",-1),p=e("p",null,"If this goes well we may look at doing further community surveys on other aspects of Pulsar.",-1),_=e("p",null,"There is no need to sign into any account to respond to this survey and no personal data is being collected (emails, names etc.).",-1),h={href:"https://docs.google.com/forms/d/e/1FAIpQLScxbP1gkvjOJ3JkdSiQm5AvBg2tqpnXZO3GxW0J6cDRlV6PJw/viewform?usp=sf_link",target:"_blank",rel:"noopener noreferrer"};function m(f,g){const n=l("ExternalLinkIcon");return r(),s("div",null,[u,a(" more "),d,p,_,e("p",null,[e("a",h,[o("You can find the Survey (Google Forms) here"),c(n)]),o(".")])])}const v=t(i,[["render",m],["__file","20230227-Daeraxa-Survey1.html.vue"]]);export{v as default}; diff --git a/assets/20230301-Daeraxa-MarUpdate.html.0a811ee1.js b/assets/20230301-Daeraxa-MarUpdate.html.0a811ee1.js new file mode 100644 index 0000000000..6c8c2d3094 --- /dev/null +++ b/assets/20230301-Daeraxa-MarUpdate.html.0a811ee1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-29fd1295","path":"/blog/20230301-Daeraxa-MarUpdate.html","title":"Community Update","lang":"en-US","frontmatter":{"title":"Community Update","author":"Daeraxa","date":"2023-03-01T00:00:00.000Z","category":["news","log"],"tag":["update"]},"excerpt":"What has the Pulsar team and community been up to lately? Find out here!
\\n","headers":[{"level":2,"title":"Community Spotlight - HTML Tim on YouTube","slug":"community-spotlight-html-tim-on-youtube","link":"#community-spotlight-html-tim-on-youtube","children":[]},{"level":2,"title":"macOS Binary Signing","slug":"macos-binary-signing","link":"#macos-binary-signing","children":[]},{"level":2,"title":"Tree-sitter Modernization","slug":"tree-sitter-modernization","link":"#tree-sitter-modernization","children":[]},{"level":2,"title":"Autocomplete CSS/HTML Automatic Updates","slug":"autocomplete-css-html-automatic-updates","link":"#autocomplete-css-html-automatic-updates","children":[]},{"level":2,"title":"Backend Version Updates","slug":"backend-version-updates","link":"#backend-version-updates","children":[]},{"level":2,"title":"Snippets Package Updates","slug":"snippets-package-updates","link":"#snippets-package-updates","children":[]},{"level":2,"title":"action-pulsar-dependency Stabilization Updates","slug":"action-pulsar-dependency-stabilization-updates","link":"#action-pulsar-dependency-stabilization-updates","children":[]}],"git":{"updatedTime":1677632078000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":5}]},"readingTime":{"minutes":2.7,"words":810},"filePathRelative":"blog/20230301-Daeraxa-MarUpdate.md","localizedDate":"March 1, 2023"}');export{e as data}; diff --git a/assets/20230301-Daeraxa-MarUpdate.html.f5a92e03.js b/assets/20230301-Daeraxa-MarUpdate.html.f5a92e03.js new file mode 100644 index 0000000000..46f97a84a0 --- /dev/null +++ b/assets/20230301-Daeraxa-MarUpdate.html.f5a92e03.js @@ -0,0 +1 @@ +import{_ as n,o as r,c as s,e as i,a as t,b as e,d as a,r as l}from"./app.0e1565ce.js";const h={},c=t("p",null,"What has the Pulsar team and community been up to lately? Find out here!",-1),d=t("h1",{id:"welcome-to-the-march-community-update",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#welcome-to-the-march-community-update","aria-hidden":"true"},"#"),e(" Welcome to the March community update")],-1),u=t("br",null,null,-1),p={href:"https://pulsar-edit.dev/blog/20230215-Daeraxa-v1.102.0.html",target:"_blank",rel:"noopener noreferrer"},m={href:"https://pulsar-edit.dev/blog/20220216-Daeraxa-ReleaseStrategyUpdate.html",target:"_blank",rel:"noopener noreferrer"},g={href:"https://pulsar-edit.dev/blog/20230227-Daeraxa-Survey1.html",target:"_blank",rel:"noopener noreferrer"},f=t("p",null,"With that said - onto the updates!",-1),b=t("h2",{id:"community-spotlight-html-tim-on-youtube",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#community-spotlight-html-tim-on-youtube","aria-hidden":"true"},"#"),e(" Community Spotlight - HTML Tim on YouTube")],-1),_={href:"https://www.youtube.com/@htmltim",target:"_blank",rel:"noopener noreferrer"},w=t("br",null,null,-1),y=t("br",null,null,-1),k={href:"https://www.youtube.com/@htmltim",target:"_blank",rel:"noopener noreferrer"},v={href:"http://htmltim.com/",target:"_blank",rel:"noopener noreferrer"},x=t("h2",{id:"macos-binary-signing",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#macos-binary-signing","aria-hidden":"true"},"#"),e(" macOS Binary Signing")],-1),S={href:"https://github.com/Meadowsys",target:"_blank",rel:"noopener noreferrer"},T=t("h2",{id:"tree-sitter-modernization",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#tree-sitter-modernization","aria-hidden":"true"},"#"),e(" Tree-sitter Modernization")],-1),P={href:"https://github.com/mauricioszabo",target:"_blank",rel:"noopener noreferrer"},M={href:"https://tree-sitter.github.io/tree-sitter/",target:"_blank",rel:"noopener noreferrer"},z=t("br",null,null,-1),H={href:"https://pulsar-edit.dev/blog/20230209-mauricioszabo-tree-sitter-part-1.html",target:"_blank",rel:"noopener noreferrer"},W=t("p",null,"With the new implementation of tree-sitter, we're gaining a better understanding of Atom's tokenizer. With this, we can simplify some of the work that will open the door to more interesting experiments, like packages that contribute to syntax elements, such as semantic highlighting.",-1),C=t("h2",{id:"autocomplete-css-html-automatic-updates",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#autocomplete-css-html-automatic-updates","aria-hidden":"true"},"#"),e(" Autocomplete CSS/HTML Automatic Updates")],-1),D=t("br",null,null,-1),I={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},L=t("br",null,null,-1),U=t("h2",{id:"backend-version-updates",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#backend-version-updates","aria-hidden":"true"},"#"),e(" Backend Version Updates")],-1),A={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},V={href:"https://github.com/Digitalone1",target:"_blank",rel:"noopener noreferrer"},B=t("br",null,null,-1),O=t("h2",{id:"snippets-package-updates",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#snippets-package-updates","aria-hidden":"true"},"#"),e(" Snippets Package Updates")],-1),j={href:"https://github.com/savetheclocktower",target:"_blank",rel:"noopener noreferrer"},N=t("br",null,null,-1),E=t("br",null,null,-1),F=t("h2",{id:"action-pulsar-dependency-stabilization-updates",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#action-pulsar-dependency-stabilization-updates","aria-hidden":"true"},"#"),e(),t("code",null,"action-pulsar-dependency"),e(" Stabilization Updates")],-1),G={href:"https://github.com/pulsar-edit/action-pulsar-dependency",target:"_blank",rel:"noopener noreferrer"},Y={href:"https://github.com/spiker985",target:"_blank",rel:"noopener noreferrer"};function R(q,J){const o=l("ExternalLinkIcon");return r(),s("div",null,[c,i(" more "),d,t("p",null,[e("Hi everyone, welcome to the March edition of our regular update posts about what is going on with the project, small wins and other details you may have missed."),u,e(" Obviously in the last month we have made some more obviously big updates such as the "),t("a",p,[e("v1.102.0 release"),a(o)]),e(" and the changes to our "),t("a",m,[e("release strategy"),a(o)]),e(" but this post is about the things that you might otherwise not have seen but still deserve to be known about.")]),t("p",null,[e("We also launched our first "),t("a",g,[e("community survey"),a(o)]),e(" which, as of time of writing, is still open. If this goes well (and depending on feedback) then we will be creating more in the future such as asking about new features, platform/use case surveys and general application feedback.")]),f,b,t("p",null,[e("First of all I want to let everyone know about "),t("a",_,[e("@htmltim"),a(o)]),e(" on YouTube. I came across his channel the other day and found a whole host of videos being made on Pulsar."),w,e(" He has covered a bunch of topics in his videos such as general overview of Pulsar, migrating from other editors, a look at a bunch of useful community packages and advice on HTML writing & editing."),y,e(" Definitely give his "),t("a",k,[e("videos"),a(o)]),e(" or "),t("a",v,[e("website"),a(o)]),e(" a look if you want to know how to get the most out of Pulsar!")]),x,t("p",null,[e("We finally have signed binaries on macOS thanks to "),t("a",S,[e("@meadowsys"),a(o)]),e(". This was mentioned as being underway last month but this has now been achieved so no misleading messages on macOS when trying to install Pulsar. From the v1.102.0 release onwards (and including all rolling releases) you will no longer have to execute a terminal command to get this to run.")]),T,t("p",null,[t("a",P,[e("@maur\xEDcio szabo"),a(o)]),e(" is still pushing ahead to get a modern implementation of "),t("a",M,[e("tree-sitter"),a(o)]),e(" working on Pulsar."),z,e(" See "),t("a",H,[e("Maur\xEDcio's blog post"),a(o)]),e(" for more info on the topic but recent wins include getting code folding working which is no small achievement.")]),W,C,t("p",null,[e("It was found that two of our core packages for HTML and CSS autocompletion had out of date or missing references. The project that was supplying the data for these completions was no longer being maintained so a new source of data had to be found."),D,e(" However it seems a like-for-like replacement from a reliable source is hard to find so "),t("a",I,[e("@confused-techie"),a(o)]),e(" has been working on this to produce a reliable replacement to get this functionality working properly again so completions remain up to date with current standards."),L,e(" These changes will hopefully be implemented shortly and included in an forthcoming update.")]),U,t("p",null,[e("A refactoring on the Pulsar package backed for a new versioning system has been underway by "),t("a",A,[e("@confused-techie"),a(o)]),e(" and "),t("a",V,[e("@digitalone1"),a(o)]),e(". The update is intended to make the system more permissive for package authors, provide easier version management and allow for the concept of release channels."),B,e(' There is also a change to add a new "VCS service system" which will allow for publishing of channels from other systems than just GitHub so authors can instead publish from the platform of their choice.')]),O,t("p",null,[t("a",j,[e("@savetheclocktower"),a(o)]),e(" has recently been heavily contributing to the project. As somebody originally involved (and experienced) with the atom/snippets package he has been adding new functionality and fixing existing issues."),N,e(" Particularly exciting updates here include the ability to map a snippet to a command as well as introducing the concept of snippet variables to Pulsar."),E,e(" Look forward to these updates in an upcoming version of Pulsar.")]),F,t("p",null,[t("a",G,[e("action-pulsar-dependency"),a(o)]),e(" is a GitHub action used for testing Pulsar packages. "),t("a",Y,[e("@spiker985"),a(o)]),e(" has been making some updates here to ensure Windows path registration and macOS symlinking is working correctly. Any GitHub CI is now able to autonomously install the most recent Pulsar rolling release for package testing.")])])}const Q=n(h,[["render",R],["__file","20230301-Daeraxa-MarUpdate.html.vue"]]);export{Q as default}; diff --git a/assets/20230315-Daeraxa-v1.103.0.html.9a66cb6e.js b/assets/20230315-Daeraxa-v1.103.0.html.9a66cb6e.js new file mode 100644 index 0000000000..ac01c21779 --- /dev/null +++ b/assets/20230315-Daeraxa-v1.103.0.html.9a66cb6e.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-6fcbebb6","path":"/blog/20230315-Daeraxa-v1.103.0.html","title":"It's that time again, Pulsar 1.103.0 is available now!","lang":"en-US","frontmatter":{"title":"It's that time again, Pulsar 1.103.0 is available now!","author":"Daeraxa","date":"2023-03-15T00:00:00.000Z","category":["dev"],"tag":["release"]},"excerpt":"Check out our newest Regular Release for Pulsar! Available Now!
We have some updates to our snippets, autocomplete-css, and github packages, which you can read about in the change log, that bring about a whole host of improvements from updated UI to new functionality to really improve your coding experience!
Of course, no update would be complete without a massive thank you to our wonderful community, especially our contributors and donors who are making all this possible.
And as ever, happy coding. See you among the stars!
autocomplete-css
to be as bleeding edge as possible.github
package.keymap.cson
.How setting license: 'none'
removed almost 100 Packages from the Pulsar Package Registry.
When this was originally done though, we had no concerns over the license of a package, at least at first.
But it wasn't long after talking to someone much more familiar with digital law, and Intellectual Property laws than anyone else on the team, we realized that re-hosting this content (even though it was just the metadata of the packages themselves, as the Pulsar Package Registry doesn't actually host the code of a package) counts as distribution, and redistribution. Which, the permission to do so is entirely governed by the package license, and the will of the author.
Now when a developer had published something to the Atom package registry it doesn't matter the license they choose. As by act of them publishing the package there, they are giving permission to Atom for distributing that content. And the same is true when a package is purposefully published to Pulsar. But because the developers of the packages we had at first had never had implicitly given us their permission, all we had was the license of their package to go off of.
The night after learning of this potential break of authors licenses, me and many others on the Pulsar team began the painstaking process of verifying the license of every single package on the Pulsar package registry. Where we had to manually check the license
field of 12,470 individual package's package.json
.
Since luckily at this point everything was in a database I was able to filter packages by the licenses they had. But even then on the initial evaluation there was 149
unique values across every single package. Where many of these simply listed License in LICENSE.md
which meant we couldn't only rely on the database, we would have to check the repository where this code lived to properly review the license itself.
Then finally, after 3 non-stop hours of reading the terms of more licenses than I ever knew existed, I had reached the first finish line, leaving me with 30 unique variations of licenses across many packages that were either a completely non-existent license, an invalid license, or simply a variation of "License in LICENSE.md"
Below you can see the abomination of a filter I wrote as I slowly went through all of the possible licenses.
And yes I'm aware that I could define an array and filter by that, but in my defense it was 1:39 AM after a few days of having my family over.
At this point there was very few unique license left. With our only entries being:
LicenseRef-LICENSE
None
[object Object]
TBD
none
SEE LICENSE IN LICENSE
NONE
Commercial Shared Source
Starting at 11:06 PM
that night, when it hit 3:17 AM
I was dead tired and left with the following results:
From here, we had our list of packages to manually verify by checking the license in their GitHub repo. Which luckily I had a huge amount of help from @Daeraxa and @Sertonix in doing this.
While this was a long and arduous process, we got through it quickly, and in the end were left with 101 packages were we would have to contact the package owner to find the next best steps, since they had no valid package for us to check.
It was at this point we littered GitHub far and wide with the following issues to every repo we had to:
Hello, this is confused-Techie from the Pulsar Team.
+
+Pulsar is an actively developed fork of Atom and one of our major efforts is to preserve the ecosystem of Community Packages that were previously published on Atom.io
+
+While we want to list every single package that existed on Atom's Package Repository, we wanted to respect the license of each package within and ensure to only list packages that either the author has agreed for us to or packages that have a license that specify that redistribution is okay.
+
+That is exactly why we are contacting you today, it seems your package <package-name> has a license that makes it hard for us to determine if we should be listing it for redistribution.
+
+All that needs to happen for you as a package maintainer is for you to give authorization for us to list your package. Just reply to this issue either way and we will respect your wishes.
+
+Otherwise within 3 weeks, if there is no response, we will assume the answer is no and remove your package from the Pulsar Package Registry.
+
+And if you've missed the deadline we have and still want your package to be published, feel free to republish to Pulsar.
+
+Thanks a ton for your time, hope you have a great day!
+
At first it was exciting to see how many package maintainers responded quickly, receiving excited and encouraging responses like:
Hello! Thanks for visiting my package, I am fine with Pulsar listing my package for redistribution.
You can publish native-ui with Pulsar. Happy if somebody is still using it!
You can publish it if you want, but to my knowledge no one uses it any more.
Hi, I'm already aware of the Pulsar project and I follow your progress... if want to redistribute to Pulsar Package REgistry now - do not hesitate. Best regards, SB
This was amazing to see, and gave us hope that we wouldn't have to remove many packages, since as you know, my biggest goal has been to ensure nothing is lost.
But, alas, when our three week self-determined timeline came to an end many issues were left unanswered. Which makes sense, when you consider some packages had not seen any updates in 8 years or more. But was still sad to see, to say the least.
But now came the time to abide by the deadline and properly respect everyone's licenses.
After quickly drafting up a script to preform a mass deletion of packages, I simply ran npm run tool:delete array
and watched as many of the packages we worked to archive were lost.
Here we have the results and analysis of our first community survey (and an infographic for those who like that kind of thing).
\\n","headers":[{"level":2,"title":"How did you hear about us?","slug":"how-did-you-hear-about-us","link":"#how-did-you-hear-about-us","children":[]},{"level":2,"title":"Which community area do you check for updates?","slug":"which-community-area-do-you-check-for-updates","link":"#which-community-area-do-you-check-for-updates","children":[]},{"level":2,"title":"Other comments on social media presence","slug":"other-comments-on-social-media-presence","link":"#other-comments-on-social-media-presence","children":[]},{"level":2,"title":"Summary","slug":"summary","link":"#summary","children":[]}],"git":{"updatedTime":1679935625000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":3.26,"words":977},"filePathRelative":"blog/20230326-Daeraxa-Survey1-Results.md","localizedDate":"March 26, 2023"}');export{e as data}; diff --git a/assets/20230401-Daeraxa-AprUpdate.html.cdce7d87.js b/assets/20230401-Daeraxa-AprUpdate.html.cdce7d87.js new file mode 100644 index 0000000000..90b5ca9026 --- /dev/null +++ b/assets/20230401-Daeraxa-AprUpdate.html.cdce7d87.js @@ -0,0 +1 @@ +import{_ as n}from"./tree-sitter.6a39e323.js";import{_ as s}from"./chocolatey.6923f762.js";import{_ as i,o as h,c as l,e as c,a as t,b as e,d as a,f as r,r as d}from"./app.0e1565ce.js";const p="/assets/i18n.77f15fa4.png",u="/assets/webassembly.dbe6d3f6.png",f="/assets/backend-badge.f36006d1.png",m="/assets/backend-badge-made.716d80d6.png",g="/assets/network.00727d52.png",b="/assets/node-package.b9535f33.png",_={},w=t("p",null,"Another dose of our regular monthly community update!",-1),y=t("h1",{id:"welcome-to-the-april-community-update-don-t-worry-no-fooling-going-on-here",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#welcome-to-the-april-community-update-don-t-worry-no-fooling-going-on-here","aria-hidden":"true"},"#"),e(" Welcome to the April Community Update (don't worry, no fooling going on here)!")],-1),k=t("p",null,"Hi everyone and welcome to the April installment of our monthly update that keeps you informed about what is going on in the background of Pulsar, work in progress, community contributions and more!",-1),v={href:"https://pulsar-edit.dev/blog/20230326-Daeraxa-Survey1-Results.html",target:"_blank",rel:"noopener noreferrer"},x={href:"https://pulsar-edit.dev/community.html",target:"_blank",rel:"noopener noreferrer"},A=r('Now that has been dealt with, onto the updates!
We will make a bigger announcement on this feature once it is ready.
Badges are not currently available in Pulsar itself but we are working on it!
An interesting update to our backend that allows for new queries to be made for finding out what packages provide or consume a given service. This can be used in a number of ways, users can easily filter by package functionality rather than just keywords and developers/packages authors can search to see what packages may provide or consume a given service to help create a more interconnected set of packages.
',4),ae={href:"https://web.pulsar-edit.dev/packages?service=terminal&serviceType=provided",target:"_blank",rel:"noopener noreferrer"},re=t("code",null,"terminal",-1),ne=t("code",null,"terminal",-1),se=r('This functionality is currently only available via the API and website query but we plan to offer some interesting and (hopefully) useful features on the website (and potentially Pulsar itself) to display these services.
ppm
Within PulsarAs you may know, ppm
is the Pulsar Package Manager
\u2014 the thing you interact with when you want to install or update packages. Most people will probably only use this by interacting with the packages menu inside Pulsar but ppm
is also supplied as a command line applications accessible via either ppm
or pulsar -p
. One issue with this approach is that we have to account for all of the different OSs and packaging methods which means adding it to $PATH
correctly on each OS etc. (This was even an issue with Atom which had a special macOS only command to install the apm
command line again).
With this change we hopefully avoid all of this as it is simply part of the main pulsar
executable so we only have to account for the details of one application and not two. This also means we can do some stuff we weren't able to easily do before, like expose the ppm
commands within Pulsar itself without having to use a terminal at all. This update has the potential to improve a lot of things so watch this space!
Another dose of our regular monthly community update!
\\n","headers":[{"level":2,"title":"i18n (internationalization) Efforts","slug":"i18n-internationalization-efforts","link":"#i18n-internationalization-efforts","children":[]},{"level":2,"title":"Tree-sitter Modernization","slug":"tree-sitter-modernization","link":"#tree-sitter-modernization","children":[]},{"level":2,"title":"TextMate Grammar Library & Superstring Migration to WASM","slug":"textmate-grammar-library-superstring-migration-to-wasm","link":"#textmate-grammar-library-superstring-migration-to-wasm","children":[]},{"level":2,"title":"Package Badges","slug":"package-badges","link":"#package-badges","children":[]},{"level":2,"title":"Package Service Filtering","slug":"package-service-filtering","link":"#package-service-filtering","children":[]},{"level":2,"title":"Bundling ppm Within Pulsar","slug":"bundling-ppm-within-pulsar","link":"#bundling-ppm-within-pulsar","children":[]},{"level":2,"title":"Chocolatey","slug":"chocolatey","link":"#chocolatey","children":[]}],"git":{"updatedTime":1680223867000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":9}]},"readingTime":{"minutes":5.91,"words":1774},"filePathRelative":"blog/20230401-Daeraxa-AprUpdate.md","localizedDate":"April 1, 2023"}');export{e as data}; diff --git a/assets/20230401-confused-Techie-PON.html.937b0317.js b/assets/20230401-confused-Techie-PON.html.937b0317.js new file mode 100644 index 0000000000..cbc241ab14 --- /dev/null +++ b/assets/20230401-confused-Techie-PON.html.937b0317.js @@ -0,0 +1,24 @@ +import{_ as a,o,c as i,e as l,a as e,b as n,d as s,f as r,r as p}from"./app.0e1565ce.js";const c={},u=e("p",null,"The release of PON!",-1),d=r(`One day during discussions amoung the Pulsar dev team, we came to the topic of what file types to support for user's config files. While it was mostly a question of how support would go with allowing anything from JSON, JSON5, TOML, YAML, CSON, and anything else we could think of, in the end we couldn't agree on a solution.
We kept finding downsides to every possibility, and concerns for support with so many. So we decided it only made sense, to take the true Atom approch, and reinvent the wheel with an in-house solution.
What we created is PON (Pulsar Object Notation)!
This new file format takes the best thing of every language and combines them into one.
Taking all of their strengths, and none of their weaknesses. Pulsar Object Notation has a more expansive feature set than any other object file format than has ever existed!
Best of all PON is easy to learn, and easy to use!
Compare the following JSON:
{
+ "core": {
+ "projectHome": "/home/dae/pulsar",
+ "welcome": {
+ "showOnStartup": true,
+ "showChangeLog": false
+ }
+ }
+}
+
The above JSON is flat out ugly. Plus, it has no support for comments, and it's nearly impossible to see which object a key belongs to without painstakingly counting each and every bracket; really JSON is an outdated, and hard to read format.
But from the impossible to use JSON above, here is the same thing in PON!
Dim core As Object
+Dim projectHome As String
+Dim welcome As Object
+Dim showOnStartup As Boolean
+Dim showChangeLog As Boolean
+
+10 # <<core>> [
+ 10.10 ## projectHome:= <</home/dae/pulsar>>
+ 10.20 ## <<welcome>> [
+ <\xBF--<<An easy to type comment!>>--?>
+ 10.20.10 ### showOnStartup:= <<unfalse>>
+ 10.20.20 ### showChangeLog:= <<untrue>>
+ ];
+];
+
The release of PON!
\\n","headers":[],"git":{"updatedTime":1680393842000,"contributors":[{"name":"confused_techie","email":"dev@lhbasics.com","commits":8},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":1.75,"words":525},"filePathRelative":"blog/20230401-confused-Techie-PON.md","localizedDate":"April 1, 2023"}');export{e as data}; diff --git a/assets/20230418-Daeraxa-v1.104.0.html.498d1fde.js b/assets/20230418-Daeraxa-v1.104.0.html.498d1fde.js new file mode 100644 index 0000000000..7cdd9b17d5 --- /dev/null +++ b/assets/20230418-Daeraxa-v1.104.0.html.498d1fde.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3a723159","path":"/blog/20230418-Daeraxa-v1.104.0.html","title":"Get Ready for another fantastically essential release, Pulsar 1.104.0 is now available!","lang":"en-US","frontmatter":{"title":"Get Ready for another fantastically essential release, Pulsar 1.104.0 is now available!","author":"Daeraxa","date":"2023-04-15T00:00:00.000Z","category":["dev"],"tag":["release"]},"excerpt":"Check out our newest Regular Release for Pulsar! Available Now!
This release contains many internal changes and upgrades, focusing on preparing the editor for much bigger changes to come.
One technical change is that we have internally patched WebComponents (document.registerElement), which will potentially fix many of our community packages that rely on this (deprecated) functionality.
We've also started our first migrations to WASM based packages (which is required for new NodeJS Versions). Any issues found here will be important to address, in order to ensure future updates go smoothly, and to unblock the road to compatibility with newer Electron versions. Please let us know if you find anything broken due to these updates!
We also have updates such as improved whitespace in a certain PHP snippet, and bleeding edge HTML completions. And with more decaf work, we should be seeing some slightly faster startup speeds.
Last but not least, we can expect to see this release being available on Chocolatey for Windows; Thank you HighHarmonics, il_mix, and COLAMAroro!
But as always we want to say a huge thanks to all those that contribute and donate to Pulsar, making it possible for us to continually release these improvements. And we want to give a special thanks to the new faces we are seeing in this update, with some brand new contributors!
As always we appreciate every single one of you, happy coding, and see you among the stars!
node-oniguruma
in favor of vscode-oniguruma
(WASMA short survey asking for community feedback on a couple of aspects of the project
\\n","headers":[],"git":{"updatedTime":1682905301000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.4,"words":120},"filePathRelative":"blog/20230430-Daeraxa-Survey2.md","localizedDate":"April 30, 2023"}');export{e as data}; diff --git a/assets/20230501-Daeraxa-MayUpdate.html.40844741.js b/assets/20230501-Daeraxa-MayUpdate.html.40844741.js new file mode 100644 index 0000000000..ee3e43c063 --- /dev/null +++ b/assets/20230501-Daeraxa-MayUpdate.html.40844741.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-638b3e6c","path":"/blog/20230501-Daeraxa-MayUpdate.html","title":"Community Update","lang":"en-US","frontmatter":{"title":"Community Update","author":"Daeraxa","date":"2023-05-01T00:00:00.000Z","category":["news","log"],"tag":["update"]},"excerpt":"Is it a bird? Is it a plane? No it's the May community update!
\\n","headers":[{"level":2,"title":"Octicons iconography update","slug":"octicons-iconography-update","link":"#octicons-iconography-update","children":[]},{"level":2,"title":"Symbols view improvements","slug":"symbols-view-improvements","link":"#symbols-view-improvements","children":[]},{"level":2,"title":"CI Testing speed upgrade","slug":"ci-testing-speed-upgrade","link":"#ci-testing-speed-upgrade","children":[]},{"level":2,"title":"Backend webhook","slug":"backend-webhook","link":"#backend-webhook","children":[]},{"level":2,"title":"Package website readme links overhaul","slug":"package-website-readme-links-overhaul","link":"#package-website-readme-links-overhaul","children":[]},{"level":2,"title":"Decoupling http handling for the package backend","slug":"decoupling-http-handling-for-the-package-backend","link":"#decoupling-http-handling-for-the-package-backend","children":[]}],"git":{"updatedTime":1683245063000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":13}]},"readingTime":{"minutes":6.49,"words":1947},"filePathRelative":"blog/20230501-Daeraxa-MayUpdate.md","localizedDate":"May 1, 2023"}`);export{e as data}; diff --git a/assets/20230501-Daeraxa-MayUpdate.html.f0636831.js b/assets/20230501-Daeraxa-MayUpdate.html.f0636831.js new file mode 100644 index 0000000000..5478f8b930 --- /dev/null +++ b/assets/20230501-Daeraxa-MayUpdate.html.f0636831.js @@ -0,0 +1,23 @@ +import{_ as o,o as i,c as r,e as l,a as e,b as t,d as s,f as a,r as c}from"./app.0e1565ce.js";const h="/assets/octicons1.9b0fa408.png",p="/assets/octicons2.69cc8255.png",d="/assets/octicons3.a3bf7b53.png",u="/assets/symbols-view.91132e73.png",m="/assets/ghtests.c6b1b477.png",g="/assets/package-webhook.7e0f3c29.png",f="/assets/package-frontend.a8bc39dd.png",k="/assets/sunsetting.aedab22a.png",b="/assets/flight-manual.36cb1750.png",v={},_=e("p",null,"Is it a bird? Is it a plane? No it's the May community update!",-1),w=e("h1",{id:"welcome-to-the-may-community-update",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#welcome-to-the-may-community-update","aria-hidden":"true"},"#"),t(" Welcome to the May Community Update")],-1),y=e("p",null,[t("Hello again and a warm welcome to our May community update! In store for you this time are Pulsar iconography updates, "),e("code",null,"symbols-view"),t(" improvements, CI upgrades and bunch of upgrades to our package backend and website.")],-1),x={href:"https://docs.google.com/forms/d/e/1FAIpQLSfnEEAQHUJbRVcH2NlNvLxmK0ZUcb5xuOCt1pYOGytvne0kJw/viewform?usp=sf_link",target:"_blank",rel:"noopener noreferrer"},j=e("p",null,"Now without further waffling, onto the updates!",-1),T=e("h2",{id:"octicons-iconography-update",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#octicons-iconography-update","aria-hidden":"true"},"#"),t(" Octicons iconography update")],-1),I={href:"https://github.com/pulsar-edit/pulsar/pull/509",target:"_blank",rel:"noopener noreferrer"},E={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},S={href:"https://primer.style/octicons/",target:"_blank",rel:"noopener noreferrer"},O=e("em",null,"really",-1),A=e("p",null,"You can see examples of the new Octicons in the below image:",-1),N=e("img",{src:h},null,-1),P=e("p",null,"One of the initial problems to work around was that Octicons no longer ships with font files, only the svg images. This meant that in order to use them they first had to be created as a font. This also meant modifying the font to make sure that the various codes aligned with the old ones so that they could, where possible, be a drop in replacement for the old version without breaking the naming scheme from upstream Octicons for use as reference material.",-1),q=e("img",{src:p},null,-1),L=e("p",null,[t("We have a couple of different goals here. The main one is simply to give more options to ourselves and users in terms of icon variation so that we can make better choices for icon usage. We would of course retain the "),e("code",null,"4.4.0"),t(" set (just as "),e("code",null,"2.1.2"),t(" has also been retained).")],-1),C={href:"https://docs.google.com/forms/d/e/1FAIpQLSfnEEAQHUJbRVcH2NlNvLxmK0ZUcb5xuOCt1pYOGytvne0kJw/viewform?usp=sf_link",target:"_blank",rel:"noopener noreferrer"},J=e("img",{src:d},null,-1),D=e("p",null,"There is still some work to go here, particularly on making sure that where icons are used we are still using the appropriate ones or selecting a more appropriate icon now it is available in the new set.",-1),H={href:"https://github.com/pulsar-edit/pulsar/blob/octicons-v18.3.0/static/icons/README.md",target:"_blank",rel:"noopener noreferrer"},U=e("h2",{id:"symbols-view-improvements",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#symbols-view-improvements","aria-hidden":"true"},"#"),t(" Symbols view improvements")],-1),W=e("img",{src:u},null,-1),F={href:"https://ctags.io/",target:"_blank",rel:"noopener noreferrer"},M=e("em",null,"at all",-1),R={href:"https://github.com/savetheclocktower",target:"_blank",rel:"noopener noreferrer"},V={href:"https://tree-sitter.github.io/tree-sitter/code-navigation-systems",target:"_blank",rel:"noopener noreferrer"},B={href:"https://microsoft.github.io/language-server-protocol/",target:"_blank",rel:"noopener noreferrer"},G=e("code",null,"symbol-view",-1),Y=e("p",null,[t("So to make steps towards achieving this we would need to work on "),e("code",null,"symbols-view"),t(" to be more generic and then update Pulsar and its packages to take advantage of the changes.")],-1),Q=e("p",null,"This has the potential to really upgrade the functionality and make navigating files and projects far more intuitive. It isn't ready just yet but don't worry, we will have more information to share on this feature in the coming weeks.",-1),K=e("h2",{id:"ci-testing-speed-upgrade",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#ci-testing-speed-upgrade","aria-hidden":"true"},"#"),t(" CI Testing speed upgrade")],-1),Z=e("img",{src:m},null,-1),z={href:"https://github.com/DeeDeeG",target:"_blank",rel:"noopener noreferrer"},X={href:"https://github.com/pulsar-edit/pulsar/pull/492",target:"_blank",rel:"noopener noreferrer"},$=e("h2",{id:"backend-webhook",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#backend-webhook","aria-hidden":"true"},"#"),t(" Backend webhook")],-1),ee=e("img",{src:g},null,-1),te={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},ne={href:"https://github.com/pulsar-edit/package-backend/pull/156",target:"_blank",rel:"noopener noreferrer"},se={href:"https://discordapp.com/channels/992103415163396136/1045131651748999239",target:"_blank",rel:"noopener noreferrer"},ae=e("h2",{id:"package-website-readme-links-overhaul",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#package-website-readme-links-overhaul","aria-hidden":"true"},"#"),t(" Package website readme links overhaul")],-1),oe=e("img",{src:f},null,-1),ie={href:"https://github.com/pulsar-edit/package-frontend/pull/96",target:"_blank",rel:"noopener noreferrer"},re={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},le=e("code",null,"package.json",-1),ce={href:"https://docs.npmjs.com/cli/v9/configuring-npm/package-json#people-fields-author-contributors",target:"_blank",rel:"noopener noreferrer"},he=a(`{
+ "author": "Barney Rubble <b@rubble.com> (http://barnyrubble.tumblr.com/)"
+}
+
Previously if this was not populated fully with all three options then the author simply wouldn't display. This fixes that problem by allowing the optional fields to be omitted (actually this is taken a step further in our implementation - even name
can be optional)
Currently the handling for HTTP and logic is all mixed together in the backend which means that nearly every function within the backend has a signature like:
async function getPackages(res, req) {...}
+
Where res
is the response object as provided by ExpressJS and req
is the request object provided by ExpressJS.
This also means when a function from the backend needs to return, it'll have a signature like:
// On Success
+res.status(200).json(data);
+logger.httpLog(req, res);
+
+// On Failure
+await common.handleError(req, res, err);
+return;
+
Which has a few downsides; when testing a function to the backend a fake Response and Request object must be made that also matches the ones provided by ExpressJS and those objects have to be monitored for the functions behavior, or otherwise the API has to be tested as just that, an API by calling the ExpressJS module itself then checking the api returns for behavior.
This also means that any new developers have to be familiar with ExpressJS to contribute and each function has to know the exact HTTP Status codes that have to be returned and in some circumstances concern themselves with header fields to set etc.
The planned changes are to allow each function to return just a serverStatusObject (which is used frequently within the backend) so we change that same signature from above to:
// On Success
+return {
+ ok: true,
+ content: data,
+};
+
+// On Failure
+return {
+ ok: false,
+ content: err,
+};
+
This has the upside of letting only the code that retrieves API requests deal with responding to them. The rest of the backend can work with just normal functions, without a care in the world about HTTP handling. This change lowers the amount of knowledge needed to contribute, so we hope that more people might be encouraged to get involved. As a bonus, it makes the code much easier to test.
This newest release of Pulsar contains many changes, from new features, to security patches, to testing improvements, this release has it all, and we hope it shows!
A big milestone for this release, is since our initial fork from Atom, we have now created fully green (passing) tests! This hopefully means we can iterate new changes, both fixes and new features, faster than ever before!
Also on this release we ensured to put a focus on solving the issues reported by our wonderful community such as resolving an issue that would prevent installation of Pulsar on RPM systems, fixing localization issues in settings-view
when browsing packages, or addressing memory leak issues within second-mate
.
Even better this release also comes with some brand new features and improvements to existing ones! Such as the addition of new community package activation hooks, adding a new bookmarks
service to the ServiceHub, and updating the internal typescript
used to support advancements within that language for community packages to take advantage of.
But this summary can't possibly include everything, make sure to take a look through the changelog below to see everything this release has to offer!
As always, we want to say a huge thanks to everyone that contributes to Pulsar, from the much appreciated donations, the critical issue reports, or amazing pull requests we receive. We appreciate every single one of you, and couldn't do any of this without you!
Till next time, happy coding, and see you among the stars!
second-mate
, fixing a memory usage issue in vscode-oniguruma
nslog
dependencyactivationHooks
, ...:uri-opened
lets a package activate when any URI is opened within Pulsar, and ...:file-name-opened
lets a package activate when any specific filename is opened within Pulsar.Welcome to a rad new release, Pulsar 1.105.0 is available now!
We asked which one was preferred out of these three mock-ups and for any additional comments people would like to make on it.
The outcome of this is fairly clear in that there is a clear preference for the filled icons. This is something we still need to work on (the pictures were only mockups) so it probably won't be in the next release but this gives a us a good indication of the direction we should go with this.
There are a few comments here I've picked out to make comment on:
Wish the icons would be like VSCode icons extension, which react to something if you name a file a certain word, like (images, root, etc) or language images like put the language images in the file, idk how to explain
As long as is isn't the outline one I'll be happy. Side note, it would be cool if the file icons showed the kind of file.
',9),_={href:"https://github.com/file-icons",target:"_blank",rel:"noopener noreferrer"},q={href:"https://web.pulsar-edit.dev/packages/search?q=file+icons",target:"_blank",rel:"noopener noreferrer"},I=t("blockquote",null,[t("p",null,"Both new versions are fine, so if possible in the future, make a setting where you can choose?")],-1),x={href:"https://pulsar-edit.dev/docs/launch-manual/sections/core-hacking/#creating-a-theme",target:"_blank",rel:"noopener noreferrer"},T=t("code",null,"Styleguide: Show",-1),A=n('Wondering why executable files (shell scripts) look the same as plain text files
We have plans to overhaul our documentation (both the site/framework and the structure of the documentation). For this we were just after some general feedback on what people like or dislike about the current docs and how it compares to other project documentation.
Whilst I can't address every comment individually, I've picked out a few to address as they are either a common question or a great point that I think needs to be addressed:
api chapter needed
Whenever I've tried to use the documentation it seems to be mostly TODO's. So, I'd say that getting the content back to where it was under Atom is more important than fine tuning layout and structure (especially the API documentation).
The API, that's the most important part of the Atom project, is not accessible. I would love to have an accessible version soon, and one that is fully searchable, possibly by showing the classes and methods as more relevant and a "full text search" also present
So the API stuff has been a little more challenging in terms of the documentation migration from the flight manual. It wasn't actually part of the flight manual but was generated from the Atom team's own API tooling which output in HTML. There were so many discussions about potential API changes and migrating to a more used/supported API docs generation tool (JSDocs) that this got left behind in fear of spending a lot of time getting it ready only to have no good way to automatically update it.
The good news is that the JSDoc work is well on its way so we should be able to get this added before too long.
Having everything as one huge page is a bit much. Maybe make the subchapters individual pages?
I think everything in Launch Manual should be moved one level up
As far as I can see this Launch Manual navigation level is not needed and would clean things up and make it more obvious where things are located by removing this level. At this time I'm not certain what else to provide feedback on.
We agree with these points and this is one of the major areas that is spurring on this rework. We don't want to do anything drastic but we do plan to make navigation a little better and less "monolithic".
',15),S={href:"https://www.youtube.com/@htmltim/videos",target:"_blank",rel:"noopener noreferrer"},W=n('Video Tutorials
We also had a bunch of comments saying they liked the current docs so we also need to take that into account rather than just going crazy and overhauling everything.
Currently, in order to publish a package to the Pulsar package registry it is a requirement to make your package available in a GitHub repository. We were after info to know if this is something that is stopping any of our community from publishing packages.
An interesting almost 50:50 split here, it makes us happy to see so many people are interested in creating packages so we will definitely take that into consideration with our documentation and processes in order to make the publishing and maintenance aspect as good as possible. I can see us asking more questions on the package development experience in the future!
So this was quite enlightening. It is good to know that most people aren't being put off by the current requirement but it is also great to find out that our plans to support other forges or services are in demand. This was very much a question designed to find out if we were a) stopping people creating packages and b) making good use of our resources by looking to expand to other platforms.
So thank you again to everyone who took part in this survey, it is fantastic to get so much community engagement on the project. Not only does it help us make informed decisions but getting comments from people clearly passionate and interested in the project is fantastic. It can be easy to get caught in a echo chamber so it is really important to get input from people who may not be as present on our various social platforms but still want to have their voice heard.
',9),P={href:"https://github.com/pulsar-edit/.github/tree/main/surveys/20230524-ProjectFeedback",target:"_blank",rel:"noopener noreferrer"},j=t("p",null,"I don't know when we will have a new survey or what the topic will be but you can be fairly sure there will be one!",-1);function D(V,C){const o=l("ExternalLinkIcon");return i(),r("div",null,[g,h(" more "),f,b,t("p",null,[e("Honestly there isn't much more to say as there wasn't really an overarching theme for this one like there was with our "),t("a",y,[e("first survey"),a(o)]),e(" so let's just get on with it shall we?")]),w,t("p",null,[e("Our first question was asking about upgrading our icons in Pulsar. We are updating our "),t("a",k,[e("Octicons"),a(o)]),e(" version bundled with Pulsar to take advantage of changes made between v4.4.0 from 2016 to the current v18.3.0. As part of this we want to explore actually using these new icons in our default UI theme. The questions below are voting on preference between two styles of the new icons against the current theme:")]),v,t("p",null,[e("These all seem to be on the concept of something like "),t("a",_,[e("file-icons"),a(o)]),e(". The Octicons font simply doesn't have support for every file icon (although it does add some extra ones we would like to include such as icons to indicate binary files or specific types of config file) so we won't be able to add this functionality in this update. The good news is that file icons can be added via a number of "),t("a",q,[e("community packages"),a(o)]),e(".")]),I,t("p",null,[e("We probably won't ship a theme for both but the outline icons will be included in the font so it would simply be a case of "),t("a",x,[e("creating a theme"),a(o)]),e(" to use the outlined ones instead of the default. All the icons will be shown in the inbuilt style guide "),T,e(' so you can easily pick and choose what ones you want. We may however look at creating a default "legacy" theme that uses the original Octicon icons as they are in Pulsar right now.')]),A,t("p",null,[e("I won't rule anything out but definitely check out "),t("a",S,[e("HTML Tim's YouTube Channel"),a(o)]),e(". He has a ton of fantastic videos on Pulsar itself, how to get the best out of it for different types of languages and info on community packages.")]),W,t("p",null,[e("As always you can find all the raw data (all checked for any personal or identifiable data) from the survey on our "),t("a",P,[e("organization repo"),a(o)]),e(".")]),j])}const L=s(m,[["render",D],["__file","20230525-Daeraxa-Survey2-Results.html.vue"]]);export{L as default}; diff --git a/assets/20230525-Daeraxa-Survey2-Results.html.af46184a.js b/assets/20230525-Daeraxa-Survey2-Results.html.af46184a.js new file mode 100644 index 0000000000..2e6528d9a1 --- /dev/null +++ b/assets/20230525-Daeraxa-Survey2-Results.html.af46184a.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-aebfc812","path":"/blog/20230525-Daeraxa-Survey2-Results.html","title":"Survey Results: Project feedback","lang":"en-US","frontmatter":{"title":"Survey Results: Project feedback","author":"Daeraxa","date":"2023-05-25T00:00:00.000Z","category":["survey"],"tag":["community","feedback"]},"excerpt":"Here we have the results and comments of our second community survey!
\\n","headers":[{"level":2,"title":"Iconography","slug":"iconography","link":"#iconography","children":[{"level":3,"title":"Comments","slug":"comments","link":"#comments","children":[]}]},{"level":2,"title":"Documentation Site Upgrade","slug":"documentation-site-upgrade","link":"#documentation-site-upgrade","children":[{"level":3,"title":"API Docs","slug":"api-docs","link":"#api-docs","children":[]},{"level":3,"title":"Layout","slug":"layout","link":"#layout","children":[]},{"level":3,"title":"Videos","slug":"videos","link":"#videos","children":[]}]},{"level":2,"title":"Community package publishing","slug":"community-package-publishing","link":"#community-package-publishing","children":[]},{"level":2,"title":"Summary","slug":"summary","link":"#summary","children":[]}],"git":{"updatedTime":1684974761000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":4.9,"words":1469},"filePathRelative":"blog/20230525-Daeraxa-Survey2-Results.md","localizedDate":"May 25, 2023"}');export{e as data}; diff --git a/assets/20230601-Daeraxa-JuneUpdate.html.5770013f.js b/assets/20230601-Daeraxa-JuneUpdate.html.5770013f.js new file mode 100644 index 0000000000..91eda23c35 --- /dev/null +++ b/assets/20230601-Daeraxa-JuneUpdate.html.5770013f.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-54f2f18e","path":"/blog/20230601-Daeraxa-JuneUpdate.html","title":"Community Update","lang":"en-US","frontmatter":{"title":"Community Update","author":"Daeraxa","date":"2023-06-01T00:00:00.000Z","category":["news","log"],"tag":["update"]},"excerpt":"What's this? No... it can't be. But it is! It's the newest instalment of the Pulsar Community Update!
\\n","headers":[{"level":2,"title":"Community package feature detection","slug":"community-package-feature-detection","link":"#community-package-feature-detection","children":[]},{"level":2,"title":"Tree-sitter updates are live!","slug":"tree-sitter-updates-are-live","link":"#tree-sitter-updates-are-live","children":[]},{"level":2,"title":"GitHub Discussions reorganisation","slug":"github-discussions-reorganisation","link":"#github-discussions-reorganisation","children":[]},{"level":2,"title":"Community spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[]}],"git":{"updatedTime":1685607621000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4}]},"readingTime":{"minutes":7.95,"words":2385},"filePathRelative":"blog/20230601-Daeraxa-JuneUpdate.md","localizedDate":"June 1, 2023"}`);export{e as data}; diff --git a/assets/20230601-Daeraxa-JuneUpdate.html.e4da287c.js b/assets/20230601-Daeraxa-JuneUpdate.html.e4da287c.js new file mode 100644 index 0000000000..e637fbadd7 --- /dev/null +++ b/assets/20230601-Daeraxa-JuneUpdate.html.e4da287c.js @@ -0,0 +1 @@ +import{_ as s}from"./detective.e7e89ed0.js";import{_ as i}from"./package.4e7449ae.js";import{_ as r}from"./tree-sitter.6a39e323.js";import{_ as l,o as h,c as u,e as d,a as e,b as t,d as a,f as n,r as c}from"./app.0e1565ce.js";const p="/assets/gh-discussions.62e6b3a4.png",g="/assets/bacadra-project-files.ca067687.png",f="/assets/bacadra-navigation-panel.e144ab63.png",m="/assets/bacadra-pdf-viewer.9f160fe1.png",b={},_=e("p",null,"What's this? No... it can't be. But it is! It's the newest instalment of the Pulsar Community Update!",-1),w=e("h1",{id:"welcome-to-the-june-community-update",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#welcome-to-the-june-community-update","aria-hidden":"true"},"#"),t(" Welcome to the June Community Update")],-1),y={href:"https://pulsar-edit.dev/blog/20230525-Daeraxa-Survey2-Results.html",target:"_blank",rel:"noopener noreferrer"},k={href:"https://github.com/orgs/pulsar-edit/discussions",target:"_blank",rel:"noopener noreferrer"},v=e("p",null,"Enough waffling, lets get on with it!",-1),x=e("h2",{id:"community-package-feature-detection",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#community-package-feature-detection","aria-hidden":"true"},"#"),t(" Community package feature detection")],-1),T=e("p",null,[e("img",{src:s,height:"200"}),e("img",{src:i,height:"200"})],-1),j={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},P={href:"https://pulsar-edit.dev/docs/launch-manual/sections/using-pulsar/#snippets",target:"_blank",rel:"noopener noreferrer"},I=e("p",null,"With this we are able to store and use information not explicitly written into the package metadata but actually in the source code of the package itself. This opens a load of possibilities in both Pulsar itself and the package website. For example you could have a filter that explicitly searches only for packages able to provide snippets or all packages that are able to provide a grammar for a particular language to the editor without just returning packages that contain the string you are looking for without the features you want.",-1),S=e("br",null,null,-1),N={href:"https://pulsar-edit.dev/blog/20230401-confused-Techie-PON.html",target:"_blank",rel:"noopener noreferrer"},A=e("code",null,".pon",-1),W=e("li",null,[t("Open my "),e("code",null,"config.pon"),t(" file for editing "),e("ul",null,[e("li",null,[t("In this case Pulsar doesn't have a grammar for PON and "),e("code",null,".pon"),t(" files so shows the language as the default "),e("code",null,"Plain Text"),t(" language")])])],-1),D=e("li",null,'Open the package install pane and search for "PON"',-1),O={href:"https://web.pulsar-edit.dev/packages/search?q=pon",target:"_blank",rel:"noopener noreferrer"},C=e("code",null,"language-pon",-1),H=e("code",null,"Install",-1),q=e("p",null,'This looks like a simple process but just look at how many results came back from searching for "PON". This shows the downside of just looking for packages via simple search terms, you have to trust that:',-1),B=e("ol",null,[e("li",null,"A language package actually exists for your language")],-1),E=e("p",null,"and",-1),U={start:"2"},F={href:"https://web.pulsar-edit.dev/packages/search?q=dockerfile",target:"_blank",rel:"noopener noreferrer"},G=e("code",null,"language-docker",-1),J=n('This new feature would allow us to avoid such a situation altogether. As we are now able to determine what packages can support a given language or file extension we can instead use a much more helpful process:
config.pon
file for editing.pon
files and displays a prompt to the user to download and install from the list of packages it has found.This approach takes the guesswork out of it completely and will only show you packages that state they have support for that filetype.
Remember this is only one example, we can extend this same behaviour to snippets and potentially other features in the future which we hope will greatly reduce the amount of guesswork and manual searching that has to be performed.
Our biggest challenge here is that we have thousands of existing packages that all need to have this analysis run on them so this won't necessarily be a quick process as we battle against GitHub's API limits but we want to make sure we have all this data ready before we go live with this feature. For new packages this action will be performed at time of publishing/update so this is a one time only thing.
We feel this is a really exciting addition so stay tuned for more info as an when we start implementing this in various places.
With the new system enabled, here are a few things you might notice:
return foo &&
in a JavaScript file and then hit Enter, a grammar could be configured to know to add a \u201Changing\u201D indent on the next line, because it knows that a valid statement in JS can\u2019t end with &&
. Or a closing brace (}
) could automatically adjust itself to match the indent level of the line with its corresponding opening brace ({
), instead of just na\xEFvely unindenting by one level.Editor: Select Larger/Smaller Syntax Node
\u2014 should work just like they did with the old Tree-sitter implementation. They\u2019re a useful way to select meaningful sections of your document \u2014 for instance, put your cursor inside of a string, press Alt + Up (or Option + Up), and the entire inside of the string is selected.And here are some other things you wouldn\u2019t notice just as a user, but would enjoy the benefits of if you contributed to the Pulsar package ecosystem:
",3),te=e("li",null,"The system for mapping Tree-sitter semantics to syntax highlighting is much more robust, which means that grammar authors now have a number of extra tools at their disposal.",-1),oe={href:"https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries",target:"_blank",rel:"noopener noreferrer"},ae=e("li",null,[t("The results of a Tree-sitter parse are useful beyond the built-in editor duties of highlighting, indenting, and folding. For instance, a Pulsar package could use Tree-sitter to find logical sections of a document, or to associate an identifier with the line where it was defined (in fact, we\u2019re planning to use some of this information to improve the "),e("code",null,"symbols-view"),t(" package and make it more flexible).")],-1),ne={href:"https://discord.gg/7aEbB9dGRT",target:"_blank",rel:"noopener noreferrer"},se={href:"https://github.com/pulsar-edit/pulsar/issues",target:"_blank",rel:"noopener noreferrer"},ie=e("h2",{id:"github-discussions-reorganisation",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#github-discussions-reorganisation","aria-hidden":"true"},"#"),t(" GitHub Discussions reorganisation")],-1),re=e("img",{src:p,height:"300"},null,-1),le={href:"https://github.com/orgs/pulsar-edit/discussions/172",target:"_blank",rel:"noopener noreferrer"},he=e("p",null,"So feel free to comment on this organisation work (positive or negative - we won't be offended) and let us know if there is anything else we could be doing to improve it.",-1),ue=e("h2",{id:"community-spotlight",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#community-spotlight","aria-hidden":"true"},"#"),t(" Community spotlight")],-1),de=e("strong",null,"@asiloisad",-1),ce={href:"https://github.com/bacadra",target:"_blank",rel:"noopener noreferrer"},pe=e("p",null,[t("To give you an idea of the scale we pulled a few stats from the backend and found that we are looking at 273 updates over 15 packages (bear in mind that this is "),e("em",null,"only"),t(" packages published or updated on the Pulsar Package Repository and doesn't include those that haven't been updated since migration from Atom).")],-1),ge=e("p",null,"Some of the packages I find particularly interesting and useful are:",-1),fe={href:"https://web.pulsar-edit.dev/packages/project-files",target:"_blank",rel:"noopener noreferrer"},me=e("img",{src:g,height:"300"},null,-1),be={href:"https://web.pulsar-edit.dev/packages/navigation-panel",target:"_blank",rel:"noopener noreferrer"},_e=e("img",{src:f,height:"300"},null,-1),we={href:"https://web.pulsar-edit.dev/packages/pdf-viewer",target:"_blank",rel:"noopener noreferrer"},ye=e("img",{src:m,height:"300"},null,-1),ke={href:"https://github.com/orgs/bacadra/repositories?q=&type=all&language=&sort=",target:"_blank",rel:"noopener noreferrer"},ve=e("p",null,[t("This is exactly the kind of thing we were hoping to achieve by continuing this project. We know the atom shutdown did a lot of damage to people's confidence in the editor and many dismissed it entirely, archived all their packages and moved on when they saw the sunsetting notice. I really hope that we get more people who are just as enthusiastic about publishing packages and just generally about getting involved in the project. So thank you again "),e("strong",null,"@asiloisad"),t(" for all the work you have been putting into this and for putting your faith into this project.")],-1),xe=e("hr",null,null,-1),Te={href:"https://pulsar-edit.dev/community.html",target:"_blank",rel:"noopener noreferrer"},je=e("hr",{class:"footnotes-sep"},null,-1),Pe={class:"footnotes"},Ie={class:"footnotes-list"},Se={id:"footnote1",class:"footnote-item"},Ne={href:"https://tree-sitter.github.io/tree-sitter/",target:"_blank",rel:"noopener noreferrer"},Ae=e("a",{href:"#footnote-ref1",class:"footnote-backref"},"\u21A9\uFE0E",-1);function We(De,Oe){const o=c("ExternalLinkIcon");return h(),u("div",null,[_,d(" more "),w,e("p",null,[t("Before we start I just want to I just want to say another big thank you for everyone who took the time to provide feedback in our recent survey for which you can find the results of in a recent "),e("a",y,[t("blog post"),a(o)]),t(". We got a lot of useful information again so if you haven't already then you can see our analysis of the results and answers to some of the questions in the previous link.")]),e("p",null,[t("We have some really exciting news in this month's edition, some automatic detective work on our backend to make life easier for everyone, updates to our "),e("a",k,[t("GitHub Discussions"),a(o)]),t(" a community spotlight on one of our most prolific package maintainers and the long await and much hyped arrival of the modernised Tree-sitter implementation!")]),v,x,T,e("p",null,[e("a",j,[t("@confused-techie"),a(o)]),t(' has been busy developing a new feature to our Pulsar Package Repository backend to add "Feature Detection". What does this mean? Well in this case feature detection refers to the analysis of community packages as and when they are published to the repository in order to collect information on what features the package can provide. For example it can determine if the package is providing '),e("a",P,[t("snippets"),a(o)]),t(" or a language grammar and, most excitingly, what languages and file extensions that grammar is able to support.")]),I,e("p",null,[t("However one of the biggest benefits is being able to use this information in Pulsar. A fairly common task when one first installs Pulsar (or any text editor) is getting it set up for the languages you use. Now, whilst Pulsar does support a large number of common grammars out of the box it can't support everything."),S,t(" So in the below example I open up a "),e("a",N,[t("PON"),a(o)]),t(" ("),A,t(") file and detail the steps needed to get it to display properly.")]),e("ul",null,[W,D,e("li",null,[t("Search through the "),e("a",O,[t("list of returned results"),a(o)]),t(" until I find "),C,t(" which looks like it might do what I want it to do and click "),H,t(".")])]),q,B,E,e("ol",U,[e("li",null,[t('The package is sensibly named and actually has your search keywords in it (for example searching for "Dockerfile" '),e("a",F,[t("returns this"),a(o)]),t(" but the package we might actually want to add dockerfile support is named "),G,t(" which is noticably absent).")])]),J,e("p",null,[t("If you have been following these monthly updates then you know that "),e("a",M,[t("tree-sitter"),a(o)]),t(" has been a common topic and one of our biggest challenges in our goals to bring Pulsar up to date.")]),e("p",null,[t("So thanks to an awful lot of work by "),e("a",R,[t("@savetheclocktower"),a(o)]),t(" and "),e("a",V,[t("@maur\xEDcio szabo"),a(o)]),t(" this project that we have been working on for months has finally hit the rolling releases in an experimental form: "),e("a",L,[t("a modernization of the Tree-sitter grammars"),a(o)]),t(".")]),e("p",null,[t("Atom was the first editor to integrate with "),e("a",z,[t("Tree-sitter"),a(o)]),t(" \u2014 a system designed to deliver fast and accurate parsing of "),e("a",K,[t("a number of programming languages"),a(o)]),t(" for better syntax highlighting than the traditional TextMate-style grammars.")]),e("p",null,[t("But being the first editor to support something new is a double-edged sword; in this case it revealed gaps in Tree-sitter functionality that were "),e("a",Q,[t("addressed in later versions"),a(o)]),t(". However, Atom\u2019s Tree-sitter support never evolved to match, and that\u2019s the situation that Pulsar inherited.")]),e("p",null,[t("Now our hand has been forced: security changes in Electron mean that the existing system needs to be converted from the "),e("a",X,[t("Node bindings"),a(o)]),t(" to the "),e("a",Y,[t("web-tree-sitter bindings"),a(o)]),t(". We have therefore treated this as an opportunity to overhaul the entire Tree-sitter system and bring it to feature parity with other modern editors that are using Tree-sitter.")]),e("p",null,[t("To start using the modern Tree-sitter implementation, first download a "),e("a",Z,[t("rolling release"),a(o)]),t(". Then find the "),$,t(" option in the Core settings pane and enable it. Any languages with built-in Tree-sitter grammars will now use the modern system.")]),ee,e("ul",null,[te,e("li",null,[t("Likewise, the systems for describing code folding and indentation will be much more user-friendly than the corresponding systems for TextMate-style grammars. Instead of having to write long, confusing regular expressions, grammar authors will often just be able to describe these behaviours in just a few lines of "),e("a",oe,[t("query statements"),a(o)]),t(".")]),ae]),e("p",null,[t("With such a large overhaul, there are bound to be bugs. If you\u2019re using the new system and things look or feel worse in any way, I can almost guarantee that it\u2019s not intentional, so please "),e("a",ne,[t("tell us about it on Discord"),a(o)]),t(" or "),e("a",se,[t("file an issue"),a(o)]),t(". If you\u2019re unsure whether it\u2019s the fault of the new Tree-sitter system, try comparing the behaviour with and without the \u201CUse Modern Tree-Sitter Implementation\u201D option enabled.")]),ie,re,e("p",null,[t("We just had a "),e("a",le,[t("revamp of our GitHub Discussions"),a(o)]),t(` which follows a feature that GitHub finally added to allow organisation via sections. This means that we have been able to better separate the various categories into more thematic groups. Hopefully this shows that we do care about and maintain the Discussions area just as much as our other social channels, we don't want people to avoid this area just because it isn't currently as active (chicken or the egg comes to mind here - it won't become more active if people don't use it.). So please, if you want to have a more "long form" discussion or you want a little more of an "asynchronous" type of engagement then Discussions is perfect for that.`)]),he,ue,e("p",null,[t("And finally we want to give a huge shout out this month to "),de,t(" (aka "),e("a",ce,[t("@bacadra"),a(o)]),t(") who has been creating and maintaining a ton of packages for Pulsar with constant updates \u2014 making them by far our most prolific package author and maintainer.")]),pe,ge,e("ul",null,[e("li",null,[e("a",fe,[t("project-files"),a(o)]),t(" - a package for providing better management and navigation of projects within Pulsar including navigation of the project list, recent projects and even adds autocompletion of path names.")])]),me,e("ul",null,[e("li",null,[e("a",be,[t("navigation-panel"),a(o)]),t(" - this provides an awful lot of stuff that is quite difficult to explain in only a few words but essentially it allows you to add custom symbols to your files")])]),_e,e("ul",null,[e("li",null,[e("a",we,[t("pdf-viewer"),a(o)]),t(" - kind of self explanatory but allows you to open up PDFs directly in Pulsar.")])]),ye,e("p",null,[t("There are tons more packages, have a look at their "),e("a",ke,[t("GitHub repos"),a(o)]),t(" for more.")]),ve,xe,e("p",null,[t("And that just about wraps things up for this month's updates. As ever if you want to get more involved the community then feel free to join in on our various "),e("a",Te,[t("social channels"),a(o)]),t(". Hope to see you again this time next month!")]),je,e("section",Pe,[e("ol",Ie,[e("li",Se,[e("p",null,[t("Image from "),e("a",Ne,[t("https://tree-sitter.github.io/tree-sitter/"),a(o)]),t(" - Copyright (c) 2018-2021 Max Brunsfeld "),Ae])])])])])}const Ee=l(b,[["render",We],["__file","20230601-Daeraxa-JuneUpdate.html.vue"]]);export{Ee as default}; diff --git a/assets/20230610-Daeraxa-2kStars.html.b0855c71.js b/assets/20230610-Daeraxa-2kStars.html.b0855c71.js new file mode 100644 index 0000000000..eff70d7cf2 --- /dev/null +++ b/assets/20230610-Daeraxa-2kStars.html.b0855c71.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,e as i,a as e,b as o,d as r,r as l}from"./app.0e1565ce.js";const c="/assets/star-history-2023610.47fda7b2.png",u={},h=e("p",null,"We just hit 2000 stars on GitHub!",-1),d=e("p",null,'This is just a quick blog post to say thank you to everyone in our wonderful community who have allowed us to get this far. I know it is "just" a number but 2000 stars shows us just how many people believe in our project.',-1),p={href:"https://github.com/pulsar-edit/pulsar",target:"_blank",rel:"noopener noreferrer"},_={href:"https://star-history.com/",target:"_blank",rel:"noopener noreferrer"},m=e("p",null,[e("img",{src:c,alt:"star-chart"})],-1),f=e("p",null,"So again, a huge thank you to everyone, particularly our wonderful community and our generous donors who make this project possible.",-1);function y(g,k){const t=l("ExternalLinkIcon");return a(),n("div",null,[h,i(" more "),d,e("p",null,[o("It is hard to believe this project is almost nearing a year old. The "),e("a",p,[o("pulsar-edit/pulsar"),r(t)]),o(" repository was created on the 12th of July 2022 and in that time we have already done so much and we look forward to doing so much more.")]),e("p",null,[o("Here is a chart generated on "),e("a",_,[o("star-history.com"),r(t)]),o(" showing our stars over time.")]),m,f])}const b=s(u,[["render",y],["__file","20230610-Daeraxa-2kStars.html.vue"]]);export{b as default}; diff --git a/assets/20230610-Daeraxa-2kStars.html.f63a03a7.js b/assets/20230610-Daeraxa-2kStars.html.f63a03a7.js new file mode 100644 index 0000000000..94f4188f92 --- /dev/null +++ b/assets/20230610-Daeraxa-2kStars.html.f63a03a7.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-25d26d73","path":"/blog/20230610-Daeraxa-2kStars.html","title":"2k Stars!","lang":"en-US","frontmatter":{"title":"2k Stars!","author":"Daeraxa","date":"2023-06-10T00:00:00.000Z","category":["news"],"tag":["github","stars"]},"excerpt":"We just hit 2000 stars on GitHub!
\\n","headers":[],"git":{"updatedTime":1686362721000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.54,"words":161},"filePathRelative":"blog/20230610-Daeraxa-2kStars.md","localizedDate":"June 10, 2023"}');export{a as data}; diff --git a/assets/20230616-Daeraxa-v1.106.0.html.22d3e3b7.js b/assets/20230616-Daeraxa-v1.106.0.html.22d3e3b7.js new file mode 100644 index 0000000000..85d950cf75 --- /dev/null +++ b/assets/20230616-Daeraxa-v1.106.0.html.22d3e3b7.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-7e31f297","path":"/blog/20230616-Daeraxa-v1.106.0.html","title":"Pulsar v1.106.0: A Focus on Grammars","lang":"en-US","frontmatter":{"title":"Pulsar v1.106.0: A Focus on Grammars","author":"Daeraxa","date":"2023-06-15T00:00:00.000Z","category":["dev"],"tag":["release"]},"excerpt":"Welcome to our latest release, Pulsar 1.106.0 is available now!
Onto our Clojure language updates, lots of this actually ties directly into the new Tree-sitter implementation as Clojure is now supported as a Tree-sitter grammar which means a whole host of new features have been added that are specific to this new implementation. We now properly support block comments, quotes and a bunch of other advanced features. Basically there has never been a better time to pick up learning Clojure in Pulsar!
Don't let these big updates make you assume we aren't also thinking about some smaller scale things, we have a tiny quality of life update to our GitHub package which adds a Log Out
option to the package options (Packages > GitHub > Log Out
) (previously only available in a somewhat obscure command - github:logout
) - just beware that this will remove your token from your system entirely so you will either need to re-enter it or create a new one to log back in.
And of course to finish up we have some annoying bugs which we have now been squashed, for example an issue that prevented images opening correctly (an issue we apparently inherited from Atom) and, whilst not a bug, a fix to solve a less than ideal situation with our new CSS autocomplete implementation to sort the suggestions in a more expected fashion. We have to thank our community for these as these last couple of items as they were brought to our attention (and in one case fixed) by some of our community members.
So that just about wraps it up for another release. As ever a huge thank you to our wonderful community and donors who make this entire project possible.
Till next time, happy coding, and see you among the stars!
autocomplete-css
Completions are now sorted in a way that may match what users expectgithub
packageThis month's update is a little different in tone to some of the others. Recently, we\u2019ve had to deal with some problems outside of our control which is why our first few items are a little more "serious" and a bit less optimistic than our usual updates. But don\u2019t despair \u2014 all the good news comes in the second half!
The "bad news" section covers our CI not building certain binaries, uncertainty about our subreddit's future, and some false antivirus positives on a Pulsar dependency, but the "good news" section cleanses the palate with a new method of downloading Pulsar as well as a bunch of improvements to the application!
So with all that covered let's get on with it!
Some of you may have noticed that we had a bit of a problem recently in producing binaries for ARM on Linux which, for the most part shouldn't have affected anyone except ARM Linux users, had a knock-on effect with some of the community-maintained packages. Some of these packages (and associated package managers) may have seen a delay receiving an update to 1.106.0.
This wasn't anything to do with our code or builds but was instead a failure of the CI machine used to produce those builds failing to properly connect to various services.
',8),y={href:"https://pulsar-edit.dev/download.html",target:"_blank",rel:"noopener noreferrer"},k=a('The reason I bring this up here is because it is already possible that some people who were using Reddit for Pulsar news have now left it and won't have seen the above post. This post also contains a poll to get feedback from the affected community to see what our next steps would be. Should we close the subreddit and abandon it? Should we open a Lemmy community instead? Should we do both?
So if you are interested in using either Reddit or Lemmy, please have a look at the above post and vote in the poll.
es5-ext
issueSome users have been reporting that various antivirus applications and virus scanning tools have been identifying Pulsar as a virus due to the es5-ext
package.
This is is nothing to worry about \u2014 Pulsar is not a virus, nor do we use dependencies that are.
After some investigation, we discovered that somewhere in our dependency tree (i.e., a package dependency of a package we use) exists this es5-ext
package. Early last year, this package was updated to display a "message of peace" to Russians (in reference to the ongoing conflict). This message displays to anyone within a Russian time zone, and, whilst the "payload" of a simple message is harmless, the act of targeting based on time zone has caused this package to be flagged by various scanners. Unfortunately the package author does not seem to want to remove it.
In our experiments, binaries built in this manner are no longer being flagged by any notable antivirus program. Hopefully this gives our users peace of mind.
We want to put Pulsar on all major package managers, but it can be trickier than it looks. Various package managers have special requirements and restrictions, so it behoves us to move slowly and make sure we understand those constraints.
',4),W={href:"https://github.com/wimpysworld/deb-get",target:"_blank",rel:"noopener noreferrer"},E=t("code",null,"deb-get",-1),T=t("code",null,".deb",-1),L=t("code",null,".deb",-1),F=t("em",null,"how",-1),P={href:"https://github.com/wimpysworld/deb-get/blob/main/01-main/packages/pulsar",target:"_blank",rel:"noopener noreferrer"},R=a('It's simple from the user's perspective, too. Once you have installed deb-get
, all you have to do to install Pulsar is:
deb-get install pulsar
To update Pulsar (and other deb-get
packages, all you need to do is:
deb-get update
deb-get upgrade
So if you are on a Debian based distro and want a quick and easy way to install and update Pulsar, give it a try!
less-cache
package updateWhat has it got in its pocketses? It's the July community update!
\\n","headers":[{"level":2,"title":"ARM builds problem","slug":"arm-builds-problem","link":"#arm-builds-problem","children":[]},{"level":2,"title":"Subreddit closure/reopening and possible Lemmy community","slug":"subreddit-closure-reopening-and-possible-lemmy-community","link":"#subreddit-closure-reopening-and-possible-lemmy-community","children":[]},{"level":2,"title":"Antivirus es5-ext issue","slug":"antivirus-es5-ext-issue","link":"#antivirus-es5-ext-issue","children":[]},{"level":2,"title":"Pulsar available on deb-get","slug":"pulsar-available-on-deb-get","link":"#pulsar-available-on-deb-get","children":[]},{"level":2,"title":"Tree-sitter migration of TOML grammar","slug":"tree-sitter-migration-of-toml-grammar","link":"#tree-sitter-migration-of-toml-grammar","children":[]},{"level":2,"title":"less-cache package update","slug":"less-cache-package-update","link":"#less-cache-package-update","children":[]}],"git":{"updatedTime":1689466216000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":18}]},"readingTime":{"minutes":5.62,"words":1687},"filePathRelative":"blog/20230701-Daeraxa-JulyUpdate.md","localizedDate":"July 1, 2023"}`);export{e as data}; diff --git a/assets/20230715-DeeDeeG-v1.107.0.html.38608ae1.js b/assets/20230715-DeeDeeG-v1.107.0.html.38608ae1.js new file mode 100644 index 0000000000..0559d33ae3 --- /dev/null +++ b/assets/20230715-DeeDeeG-v1.107.0.html.38608ae1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5e247e06","path":"/blog/20230715-DeeDeeG-v1.107.0.html","title":"Fresh off the CI press: Pulsar 1.107.0 is available now!","lang":"en-US","frontmatter":{"title":"Fresh off the CI press: Pulsar 1.107.0 is available now!","author":"Daeraxa","date":"2023-07-15T00:00:00.000Z","category":["dev"],"tag":["release"]},"excerpt":"Fresh off the CI press: Pulsar 1.107.0 is available now!
Hotfix: Pulsar 1.107.1 is available now!
As always, a big, warm welcome to this month's update! This time around we have some interesting new features, some of which have been in the works for a while, so it is great to be able to report on their state as they will soon be making their way to a Pulsar near you! These updates include news on a new "pulsar-updater" package designed to help you keep up to date with our latest releases, a big new feature for one of our core packages for working with Markdown documents and bringing some of our dependencies up to date so you no longer have to keep around old toolchains.
So lets get on with it!
This is done by providing a "language identifier" to the codeblock in order to signal to Pulsar what language you want it to highlight. For example, if you wanted to embed some javascript in a document, you would need to start the code fence like this:
```javascript\n
or
```js\n
The issue was that this list of identifiers had not been updated for a long time, and people used to other tools and applications with this same functionality (for example, within GitHub and various static site generators) were finding that identifiers they commonly used were not being correctly identified.
',5),x=t("em",null,"only",-1),P={href:"https://github.com/github-linguist/linguist/blob/master/lib/linguist/languages.yml",target:"_blank",rel:"noopener noreferrer"},D=t("code",null,"markdown-preview",-1),N=t("code",null,"Custom Syntax Highlighting Language Identifiers",-1),I={href:"https://web.pulsar-edit.dev/packages/language-pon",target:"_blank",rel:"noopener noreferrer"},B=t("code",null,"j",-1),G=t("code",null,"pon: source.pon, j: source.js",-1),Q={href:"https://pulsar-edit.dev/download.html#rolling-release",target:"_blank",rel:"noopener noreferrer"},E={href:"https://pulsar-edit.dev/download.html#regular-releases",target:"_blank",rel:"noopener noreferrer"},V=t("h2",{id:"moving-ppm-to-our-own-npm-fork",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#moving-ppm-to-our-own-npm-fork","aria-hidden":"true"},"#"),e(" Moving PPM to our own NPM fork")],-1),j=t("img",{src:g,height:"200"},null,-1),T={href:"https://github.com/nodejs/node-gyp/tree/main",target:"_blank",rel:"noopener noreferrer"},S=t("code",null,"node-gyp",-1),U={href:"https://github.com/pulsar-edit/ppm",target:"_blank",rel:"noopener noreferrer"},C={href:"https://github.com/DeeDeeG",target:"_blank",rel:"noopener noreferrer"},M={href:"https://github.com/pulsar-edit/ppm/pull/79",target:"_blank",rel:"noopener noreferrer"},F=t("code",null,"node-gyp",-1),H=t("h2",{id:"pulsar-updater-package-in-the-works",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#pulsar-updater-package-in-the-works","aria-hidden":"true"},"#"),e(" pulsar-updater package in the works")],-1),O=t("img",{src:m,height:"200"},null,-1),R=t("p",null,'A comment that we see come up somewhat regularly from both our team and community members alike is how Pulsar should deal with updates. Atom has an auto-update module that (at least on Windows and macOS) would allow you to update Atom from within the application. Unfortunately, these existing auto-update methods are either incompatible or unfeasible to implement within Pulsar (for example, the requirement that we would need to pay large amounts of money to sign our binaries). We also support (and plan to support) a much larger set of distribution methods than Atom - various Linux, Windows and macOS package managers as well as our "standard" binary releases, which means accounting for all of these different methods.',-1),X={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},q={href:"https://github.com/pulsar-edit/pulsar/pull/656",target:"_blank",rel:"noopener noreferrer"},Y=t("p",null,"The notifications generated by this package can be dismissed entirely or until the next launch in order to be as minimally intrusive as possible.",-1),J=t("p",null,"This feature is still something in the works so isn't available just yet, but you can be sure we will provide more details and an announcement once it is out in the wild.",-1),K=t("h2",{id:"pulsar-package-repository-feature-detection-is-now-live",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#pulsar-package-repository-feature-detection-is-now-live","aria-hidden":"true"},"#"),e(" Pulsar Package Repository feature detection is now live!")],-1),z=t("p",null,[t("img",{src:r,height:"200"}),t("img",{src:i,height:"200"})],-1),Z={href:"https://pulsar-edit.dev/blog/20230601-Daeraxa-JuneUpdate.html#community-package-feature-detection",target:"_blank",rel:"noopener noreferrer"},L=t("p",null,"We don't currently have any implementations of this feature in Pulsar itself just yet, but we do have plans for it. For example, we could provide automatic detection of a community grammar for a language not installed on the system. This could easily be extended to all kinds of other features provided by community packages.",-1),W=t("h2",{id:"community-spotlight",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#community-spotlight","aria-hidden":"true"},"#"),e(" Community spotlight")],-1),$=t("img",{src:s,height:"200"},null,-1),ee={href:"https://github.com/arite",target:"_blank",rel:"noopener noreferrer"},te={href:"https://github.com/cat-master21",target:"_blank",rel:"noopener noreferrer"},oe={href:"https://github.com/2colours",target:"_blank",rel:"noopener noreferrer"},ae={href:"https://github.com/GuilleW",target:"_blank",rel:"noopener noreferrer"},ne=t("hr",null,null,-1),re={href:"https://pulsar-edit.dev/community.html",target:"_blank",rel:"noopener noreferrer"};function ie(se,le){const o=c("ExternalLinkIcon");return h(),d("div",null,[w,u(" more "),A,t("p",null,[t("a",b,[e("@confused-techie"),a(o)]),e(" has been working on a significant update to the way the "),y,e(" package handles the highlighting of code inside code fences. The "),v,e(' package is a package included with Pulsar that allows you to render Markdown documents directly within Pulsar without the need for any external tools. One of its features is the fact that it is able to perform syntax highlighting of code within "code fences" or "code blocks". That is, if you include a small snippet of javascript or shell script into your document, '),_,e(" can provide the same level of syntax highlighting to your snippet in the preview pane as in the main editor.")]),k,t("p",null,[e("This change adds some significant improvements to this system, not content with "),x,e(" updating the existing list of languages to match GitHub's "),t("a",P,[e("Linguist"),a(o)]),e(' library but massively extending the system to allow users to tailor the behaviour to suit their own needs. In addition to the "Linguist" (GitHub) mode, it adds in lists for other tools such as "Chroma" (Codeberg, Gitea, Hugo), "Rouge" (GitLab, Jekyll) and "HighlightJS" (Markdown-IT). In many cases, these will be identical and users can leave it on the existing default mode (Linguist). However, for those who rely on Pulsar and the '),D,e(" package to correctly display code as it will be used on the platform of their choice, this allows them to not have to modify the identifier between their editor and their production files.")]),t("p",null,[e("As if that wasn't enough, it also features a new feature that allows you to freely add and customise your own options in order to include non-default languages and to override your own options by way of a new setting within the package: \xA0"),N,e(". For example, if I wanted to have "),t("a",I,[e("PON"),a(o)]),e(" support or wanted to add "),B,e(' as an identifier for "javascript", I could add the following to that setting: '),G,e(".")]),t("p",null,[e("You can try out this feature right now in our "),t("a",Q,[e("rolling release"),a(o)]),e(" or you can wait until our next "),t("a",E,[e("regular release"),a(o)]),e(".")]),V,j,t("p",null,[e("One issue we have had crop up again and again that has frustrated contributors and users alike is that Pulsar was using some older versions of some tools, particularly "),t("a",T,[e("node-gyp"),a(o)]),e(" (a tool for compiling native modules for Node.js), which had some very particular requirements for development tooling on the machine. For example, it was very particular about which version of the Visual Studio tools would work and which versions of Python could be used (i.e. not the latest releases).")]),t("p",null,[e("This is a problem for Pulsar because "),S,e(" is used by NPM which is used as part of our "),t("a",U,[e("PPM"),a(o)]),e(" (Pulsar Package Manager) tool for installing Pulsar packages. Pulsar does not use a system NPM installation but instead has its own version bundled with the application.")]),t("p",null,[e("So "),t("a",C,[e("@DeeDeeG"),a(o)]),e(" has done some work to "),t("a",M,[e("migrate to our own fork of NPM"),a(o)]),e(" which changes our "),F,e(" dependency from the old 5.11 to the much newer 9.4.0 which should mean that Pulsar will soon properly support modern compiler toolchains and drop support for old, unsupported ones.")]),H,O,R,t("p",null,[e("So a first step towards a solution here is a new core package developed by "),t("a",X,[e("@confused-techie"),a(o)]),e(" called "),t("a",q,[e("pulsar-updater"),a(o)]),e(" which is designed to inform the user not only when a new version has been released but also how to install it based on what install method is detected on the system. For example, it might prompt you to visit the Pulsar website to download a new binary, or it can provide a command for your terminal to run.")]),Y,J,K,z,t("p",null,[e("A couple of months ago we announced that a "),t("a",Z,[e("new feature was coming to our package backend"),a(o)]),e('. The linked post goes into far more detail, but in a nutshell this is a feature that adds "feature detection" to our backend, allowing users and developers to have a much more cohesive experience when trying to install or develop new Pulsar packages that rely on features from other community packages.')]),L,W,$,t("p",null,[e("As we always say (because it is absolutely true), this project would be nothing if it weren't for our wonderful community members, and we want to make sure we provide proper attribution when people directly contribute to Pulsar, not only through our release notes but on our social channels as well. So in light of that, a big thank you to "),t("a",ee,[e("@arite"),a(o)]),e(", "),t("a",te,[e("@cat-master21"),a(o)]),e(", "),t("a",oe,[e("@2colors"),a(o)]),e(" and "),t("a",ae,[e("@GuilleW"),a(o)]),e(" who have made contributions to the project which we have recently included.")]),ne,t("p",null,[e("And just like that, we are done with yet another month's updates. As ever, if you want to get more involved in the community, please join in on our various "),t("a",re,[e("social channels"),a(o)]),e(". Hope to see you again this time next month!")])])}const pe=l(f,[["render",ie],["__file","20230801-Daeraxa-AugustUpdate.html.vue"]]);export{pe as default}; diff --git a/assets/20230801-Daeraxa-AugustUpdate.html.a30019be.js b/assets/20230801-Daeraxa-AugustUpdate.html.a30019be.js new file mode 100644 index 0000000000..32886257db --- /dev/null +++ b/assets/20230801-Daeraxa-AugustUpdate.html.a30019be.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3cb6722f","path":"/blog/20230801-Daeraxa-AugustUpdate.html","title":"Community Update","lang":"en-US","frontmatter":{"title":"Community Update","author":"Daeraxa","date":"2023-08-01T00:00:00.000Z","category":["news","log"],"tag":["update"]},"excerpt":"How many roads must a man walk down? Not a clue, but what I do know is that this is the one and only Pulsar Community Update!
\\n","headers":[{"level":2,"title":"Update to markdown-preview language identifiers","slug":"update-to-markdown-preview-language-identifiers","link":"#update-to-markdown-preview-language-identifiers","children":[]},{"level":2,"title":"Moving PPM to our own NPM fork","slug":"moving-ppm-to-our-own-npm-fork","link":"#moving-ppm-to-our-own-npm-fork","children":[]},{"level":2,"title":"pulsar-updater package in the works","slug":"pulsar-updater-package-in-the-works","link":"#pulsar-updater-package-in-the-works","children":[]},{"level":2,"title":"Pulsar Package Repository feature detection is now live!","slug":"pulsar-package-repository-feature-detection-is-now-live","link":"#pulsar-package-repository-feature-detection-is-now-live","children":[]},{"level":2,"title":"Community spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[]}],"git":{"updatedTime":1691032635000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":4.95,"words":1486},"filePathRelative":"blog/20230801-Daeraxa-AugustUpdate.md","localizedDate":"August 1, 2023"}');export{e as data}; diff --git a/assets/20230816-DeeDeeG-v1.108.0.html.11a20d1e.js b/assets/20230816-DeeDeeG-v1.108.0.html.11a20d1e.js new file mode 100644 index 0000000000..cfd1bfd1f1 --- /dev/null +++ b/assets/20230816-DeeDeeG-v1.108.0.html.11a20d1e.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0730d41e","path":"/blog/20230816-DeeDeeG-v1.108.0.html","title":"Great Scott! A new Pulsar release. Pulsar 1.108.0 is available now!","lang":"en-US","frontmatter":{"title":"Great Scott! A new Pulsar release. Pulsar 1.108.0 is available now!","author":"Daeraxa","date":"2023-08-16T00:00:00.000Z","category":["dev"],"tag":["release"]},"excerpt":"Great Scott! A new Pulsar release. Pulsar 1.108.0 is available now!
Last, but definitely not least, we have finally begun the migration from apm
to ppm
. When we started the Pulsar project, one of the first things we did was rename the "Atom Package Manager" to the "Pulsar Package Manager" for obvious reasons. However, under the hood, you would still find apm
binaries and files; this was particularly relevant for Windows users, for which there was no automatic PATH
registration. The good news is that we have begun the transition to change them. For the time being, apm
will remain alongside ppm
until we can be sure we haven't hit any unforeseen issues.
And with that, we bring this to a close. As ever, a huge thank you to all of our community members and especially our donors, without whom this project just wouldn't be possible.
Until next time, happy coding, and see you amongst the stars!
less
files in packages to use inline JavaScript inside backticks.styleguide
package.#is?
and #is-not?
where applicable.markdown-preview
that adds support for Linguist, Chroma, Rouge, and HighlightJS for language identifiers in fenced code blocks.TextMate
language-toml
grammar to properly support whitespace where-ever it may appear.pulsar-updater
to help users update Pulsar.ppm
and ppm.cmd
binaries/launchers within ppm. This allows easier integration of correctly named binaries on more systems in more contexts (especially Windows). Existing apm
and apm.cmd
binaries/launchers are still there for the time being.An update on the status of our Chocolatey packages.
\\n","headers":[],"git":{"updatedTime":1693700588000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":1.08,"words":324},"filePathRelative":"blog/20230825-Daeraxa-ChocolateyUpdate.md","localizedDate":"August 25, 2023"}');export{a as data}; diff --git a/assets/20230825-Daeraxa-ChocolateyUpdate.html.914c06fc.js b/assets/20230825-Daeraxa-ChocolateyUpdate.html.914c06fc.js new file mode 100644 index 0000000000..cba124ce12 --- /dev/null +++ b/assets/20230825-Daeraxa-ChocolateyUpdate.html.914c06fc.js @@ -0,0 +1 @@ +import{_ as n}from"./chocolatey.6923f762.js";import{_ as s,o as r,c as l,e as i,a as e,b as o,d as a,r as c}from"./app.0e1565ce.js";const h="/assets/choco-pulsar.4fa6b61c.png",u="/assets/choco-versions.c7569503.png",d={},p=e("p",null,"An update on the status of our Chocolatey packages.",-1),_=e("img",{src:n,height:"150"},null,-1),f={href:"https://chocolatey.org/",target:"_blank",rel:"noopener noreferrer"},g={href:"https://github.com/COLAMAroro",target:"_blank",rel:"noopener noreferrer"},m={href:"https://github.com/spiker985",target:"_blank",rel:"noopener noreferrer"},w={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},y=e("p",null,[e("img",{src:h,height:"300"}),e("img",{src:u,height:"300"})],-1),b=e("p",null,"Whilst all this was going on, we also realised that the process of actually creating a new release for Chocolatey was a little inefficient and overall too manual, so we took the opportunity to also upgrade our release process into a semi-automated one so that all we have to do is press a few buttons and all the rest is done automatically.",-1),k=e("p",null,"Thank you to all those who have been waiting patiently for this issue to be resolved. With any luck, now that we have our processes for this ironed out, we won't come across anything like this again.",-1),v=e("p",null,[o("So now you can safely run a cheeky "),e("code",null,"choco upgrade pulsar"),o(" at your convenience to get the latest release installed on your system and enjoy all the updates and upgrades we have been putting into Pulsar these last few months.")],-1);function x(C,A){const t=c("ExternalLinkIcon");return r(),l("div",null,[p,i(" more "),_,e("p",null,[o("As you may know, we decided to officially support the "),e("a",f,[o("Chocolatey"),a(t)]),o(' package manager on Windows back in April. However, we quickly ran into a few problems regarding the Pulsar binaries exceeding the size allowed. Unfortunately, this meant that Pulsar was "stuck" on version 1.103.0 whilst we tried to find a solution.')]),e("p",null,[o("Unfortunately, this went on for rather longer than expected, but thanks to the efforts of a number of people, including "),e("a",g,[o("@COLAMAroro"),a(t)]),o(", "),e("a",m,[o("@spiker985"),a(t)]),o(" and "),e("a",w,[o("@confused-techie"),a(t)]),o(", a solution was found to the problem, and, as a result, the latest regular release of Pulsar is once again available on Chocolatey. Not only that, but all the intermediate releases have also been provided.")]),y,b,k,v])}const P=s(d,[["render",x],["__file","20230825-Daeraxa-ChocolateyUpdate.html.vue"]]);export{P as default}; diff --git a/assets/20230903-confused-Techie-pulsars-ci.html.c905c8d2.js b/assets/20230903-confused-Techie-pulsars-ci.html.c905c8d2.js new file mode 100644 index 0000000000..79972842d9 --- /dev/null +++ b/assets/20230903-confused-Techie-pulsars-ci.html.c905c8d2.js @@ -0,0 +1 @@ +import{_ as s,o as a,c as n,e as l,a as t,b as e,d as r,f as i,r as d}from"./app.0e1565ce.js";const h={},u=t("p",null,"How Pulsar's CI has recently changed.",-1),c=i('CI, or Continuous Integration, is a rather broad term used to describe the DevOps process of continuously (oftentimes on every change) building and testing a piece of software. In Pulsar's case this means two big things:
To better understand how drastically things have changed, let's quickly take a look how Pulsar's CI has looked up until this month since nearly the beginning of the project.
Whenever someone made a Pull Request to Pulsar, their code changes would be run through a gauntlet of processes:
The big point of interest today is the process of building the Pulsar binaries.
',7),p={href:"https://github.com/mauricioszabo",target:"_blank",rel:"noopener noreferrer"},g={href:"https://github.com/pulsar-edit/pulsar/commit/64427f41782ae4a2ab72a81762dc8f2bcb3f2f7e",target:"_blank",rel:"noopener noreferrer"},f={href:"https://cirrus-ci.com/",target:"_blank",rel:"noopener noreferrer"},w={href:"https://opencollective.com/pulsar-edit",target:"_blank",rel:"noopener noreferrer"},m={href:"https://github.com/sponsors/pulsar-edit",target:"_blank",rel:"noopener noreferrer"},b={href:"https://cirrus-ci.org/blog/2023/07/17/limiting-free-usage-of-cirrus-ci/",target:"_blank",rel:"noopener noreferrer"},y=t("h2",{id:"how-it-is-and-how-it-got-there",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#how-it-is-and-how-it-got-there","aria-hidden":"true"},"#"),e(" How it is, and how it got there")],-1),v=t("p",null,"Now, I don't want to come across as ungrateful to the fantastic service that Cirrus offers, so when this price change was first announced, we had originally intended to figure out if we could afford these price changes reasonably.",-1),_={href:"https://github.com/DeeDeeG",target:"_blank",rel:"noopener noreferrer"},k=i("Keeping in mind that we trigger the same amount of runs per platform, the difference here is how slow or fast any given platform is during the build process:
Month | Windows Credits/Hours | Linux Credits/Hours | macOS Credits/Hours |
---|---|---|---|
May | 66 credits / 273 hours | 72 credits / 396 hours | 490 credits / 544 hours |
July | 55 credits / 229 hours | 39 credits / 213 hours | 305 credits / 339 hours |
Using this data, where we consider 1 credit = $1 USD, minus the novel 50 free credits, July would've cost $349 USD. We could then realistically estimate that building Pulsar binaries will cost ~$350 USD a month.
Which when we consider that's a higher price than anything the Pulsar team spends any money on for Pulsar, it's a bit of a tough pill to swallow. But especially because Cirrus isn't the only service out there, no one on the team could justify spending our sponsor's money on such a large expense. At this point, we had to determine a way to lower our usage or move off the platform.
But to move off Cirrus, we had to consider everything it currently does for us:
master
)download
microservice redirects downloads directly to Cirrus)At this point, with the above knowledge, we had several different solutions to jugle and consider:
In finding a solution to our platform support issue, we looked at quite a few possibilities, at a point we were even considering purchasing an Apple Silicon Mac, second hand, and using GitHub Self Hosted Runners to build there. But of course the simplest answer, would be to only build Apple Silicon on Cirrus, but we had to consider what that would actually come out to. Looking at using all 50 free credits for building only Apple Silicon binaries, would mean getting up to 833.333 minutes within Cirrus, but we had to compare those minutes into what we could acheive:
Typical Apple Silicon task run lengths
Run Type | Length in Minutes | Task runs/month |
---|---|---|
Shortest Recorded | 14.35 | 58 |
Average | 17.947 | 46 |
Median | 17.292 | 48 |
Longest Recorded | 26.6 | 31 |
So we could estimate to say we could have anywhere from 31-58 macOS runs per month (including re-runs for failures), but that shooting for ~30 macOS runs per month is a safer conservative estimate.
But, all of this above doesn't account for our ARM Linux runs, that also must stay at Cirrus CI. So we had to consider, within our 50 free credits per month that would get us 8333.333 minutes of Linux builds per month (Which that is not a typo, we are able to get 10x the amount of real-world macOS minutes compared to ARM Linux):
Typical ARM Linux task run lengths
Run Type | Length in Minutes | Task runs/month |
---|---|---|
Shortest Recorded | 28.367 | 293 |
Median | 30.208 | 275 |
Average | 32.114 | 259 |
Longest Recorded | 45.883 | 181 |
So while it seems we could build these platforms on Cirrus, there was temporarily a concern about if we could choose only one, to keep at our current pace, at this point we had to try and find if we should give any specific platform any priority, to which we turned to our install numbers (gathered from the logs of the download
microservice):
Rolling Release Download count by Platform over the past 30 days:
Platform | Download Count | Download Percentage |
---|---|---|
Apple Silicon | 860 | 15.59% |
ARM Linux | 109 | 1.97% |
Windows | 2,370 | 42.98% |
Intel Mac | 550 | 9.97% |
Linux | 1,624 | 29.45% |
With this, we could easily see that if one platform must be prioritized, the numbers say that priority should be given to Apple Silicon, even though, we knew we didn't want to play favourites with platforms.
It was at this point, we had to break down how many runs we could do for both platforms. If we assume each build is one Apple Silicon macOS run and one ARM Linux run:
Credit-Cost Build Type | Credits | Builds/month |
---|---|---|
Lowest Recorded | 1.032 | 48 |
Median | 1.256 | 39 |
Average | 1.269 | 39 |
Highest Recorded | 1.766 | 28 |
With these exact numbers of our current trends, we could then assume we have an allotment of 28 builds per month for these platforms. But consdering the possible need of retries, or a build erroring out, we had settled on building Apple Silicon, and ARM Linux once every other day. Which should give us about 15 builds per month minimum. This would give us a healthy buffer zone, or any retries needed, and the ability to do another run for the regular release every month.
But even though we now knew what we needed to do, there was still a long list of things we had to change to get there, especially now considering our deadline to do so was only a few days away.
After a flurry of PRs and lots of testing, the Pulsar team was able to accomplish all of the following in just under our 5 day limit:
pulsar-rolling-releases
to act as the CDN for every single rolling releasedownload
microservice to now use this new repository to retreive the newest rolling releaseHow Pulsar's CI has recently changed.
\\n","headers":[{"level":2,"title":"How it was","slug":"how-it-was","link":"#how-it-was","children":[]},{"level":2,"title":"How it is, and how it got there","slug":"how-it-is-and-how-it-got-there","link":"#how-it-is-and-how-it-got-there","children":[]},{"level":2,"title":"Summary","slug":"summary","link":"#summary","children":[]}],"git":{"updatedTime":1694135719000,"contributors":[{"name":"confused_techie","email":"dev@lhbasics.com","commits":15},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":6.43,"words":1930},"filePathRelative":"blog/20230903-confused-Techie-pulsars-ci.md","localizedDate":"September 7, 2023"}`);export{e as data}; diff --git a/assets/20230904-Daeraxa-SeptemberUpdate.html.055d9ca9.js b/assets/20230904-Daeraxa-SeptemberUpdate.html.055d9ca9.js new file mode 100644 index 0000000000..1337247bc9 --- /dev/null +++ b/assets/20230904-Daeraxa-SeptemberUpdate.html.055d9ca9.js @@ -0,0 +1 @@ +import{_ as n}from"./chocolatey.6923f762.js";import{_ as r}from"./package.4e7449ae.js";import{_ as s}from"./spotlight.aba19e76.js";import{_ as i,o as l,c as h,e as u,a as e,b as t,d as a,f as c,r as d}from"./app.0e1565ce.js";const p="/assets/moving.0cfb3518.png",m="/assets/title-bar-tab.c7644600.png",f="/assets/title-bar-no-tab.2e70eb94.png",g="/assets/cleaning.f5279602.png",b={},_=e("p",null,"I want you to act as a Pulsar team member writing a blog post. I will provide titles of recent changes. You will elaborate on each topic and make each paragraph coherent from my ramblings. You will welcome all readers to the September Pulsar community update.",-1),w=c('Welcome to this month's Pulsar community update! In store for you this month we have some massive changes to our CI process, some good news for Windows Chocolatey users, a new option for Pulsar's title bar, some improvements to our ppm unpublish
command and work on a brand new utility to help clean up elements of a Pulsar installation.
I realise that this probably raises more questions than it answers, so read on to find out more!
Ever since we started providing downloadable binaries shortly after the Pulsar project properly started, we have been using Cirrus CI as our continuous integration platform. This has worked very well for us for quite some time now, but unfortunately for us, they recently decided to change their free tier and our setup puts us well beyond that free tier and into some quite serious money. At this point we had a couple of choices: either we stick with Cirrus CI by using our donors' money, or we move to another platform. We chose the latter option as we simply could not justify nor sustain the costs.
',6),y={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},k={href:"https://github.com/DeeDeeG",target:"_blank",rel:"noopener noreferrer"},v={href:"https://github.com/Meadowsys",target:"_blank",rel:"noopener noreferrer"},x=e("p",null,"So what does this mean for our community? Hopefully, you didn't even notice anything. The biggest effect is that Apple silicon macOS and ARM Linux binaries are produced a little less frequently (i.e.once every other day).",-1),T=e("p",null,"We have a much bigger and more detailed write-up on what went on to be published soon, as it was a particularly interesting problem (for a community project funded entirely by donations) to work around, so watch this space!",-1),C=e("h2",{id:"chocolately-packages-are-up-to-date-again",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#chocolately-packages-are-up-to-date-again","aria-hidden":"true"},"#"),t(" Chocolately packages are up to date again!")],-1),P=e("img",{src:n,height:"150"},null,-1),A={href:"https://github.com/COLAMAroro",target:"_blank",rel:"noopener noreferrer"},I={href:"https://github.com/spiker985",target:"_blank",rel:"noopener noreferrer"},W={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},S={href:"https://pulsar-edit.dev/blog/20230825-Daeraxa-ChocolateyUpdate.html",target:"_blank",rel:"noopener noreferrer"},j=e("h2",{id:"new-title-bar-configuration-option",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#new-title-bar-configuration-option","aria-hidden":"true"},"#"),t(" New title bar configuration option")],-1),M={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},D={href:"https://github.com/pulsar-edit/pulsar/pull/671",target:"_blank",rel:"noopener noreferrer"},N=e("p",null,"To demonstrate, the next image shows what it currently looks like:",-1),U=e("img",{src:m,height:"50"},null,-1),L=e("p",null,"And what it looks like if you turn the setting off to remove the tab from the title:",-1),E=e("img",{src:f,height:"50"},null,-1),V={href:"https://pulsar-edit.dev/download.html#rolling-release",target:"_blank",rel:"noopener noreferrer"},q=e("h2",{id:"unpublishing-packages",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#unpublishing-packages","aria-hidden":"true"},"#"),t(" Unpublishing packages")],-1),O=e("img",{src:r,height:"200"},null,-1),R={href:"https://github.com/bacadra/",target:"_blank",rel:"noopener noreferrer"},B={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},G={href:"https://github.com/pulsar-edit/package-backend/pull/195",target:"_blank",rel:"noopener noreferrer"},H=e("p",null,[t("However, shortly after this was fixed, we were contacted by a community member asking why they were unable to re-publish their package after a successful unpublish in order to fix a versioning mistake. Ultimately, we realised that the wording shown during the unpublishing process does not make it clear that once a package has been unpublished, we "),e("em",null,"permanently block the package name from being used ever again"),t(". This is intentional in order to prevent any bad actors from hijacking the name of a previous package in order to hide malicious code. This also provided the opportunity for us to come up with a process to work around this in exceptional circumstances, we will now ensure that:")],-1),Y=e("ol",null,[e("li",null,"Nobody has downloaded the unpublished package."),e("li",null,"There are no significant code changes between the unpublished version and the version the author wishes to re-publish."),e("li",null,"The author can prove ownership of the repository.")],-1),z=e("code",null,"unpublish",-1),$={href:"https://github.com/pulsar-edit/package-backend/pull/198",target:"_blank",rel:"noopener noreferrer"},F=e("h2",{id:"pulsar-cleanup",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#pulsar-cleanup","aria-hidden":"true"},"#"),t(" Pulsar cleanup")],-1),J=e("img",{src:g,height:"200"},null,-1),K={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},Q={href:"https://github.com/pulsar-edit/pulsar-cleanup",target:"_blank",rel:"noopener noreferrer"},X=e("code",null,"electron-builder",-1),Z=e("h2",{id:"community-spotlight",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#community-spotlight","aria-hidden":"true"},"#"),t(" Community spotlight")],-1),ee=e("img",{src:s,height:"200"},null,-1),te={href:"https://github.com/casswedson",target:"_blank",rel:"noopener noreferrer"},oe={href:"https://github.com/pulsar-edit/pulsar/pull/678",target:"_blank",rel:"noopener noreferrer"},ae=e("hr",null,null,-1),ne=e("p",null,"And yet again that brings us to the end of another community update. We hope you enjoyed this content and that you are looking forward to seeing some of these changes coming to a Pulsar near you! As ever, it would be amiss of us to not mention our amazing community and especially our financial donors, who without this project would simply not be possible! So thank you again and see you all again this time next month!",-1);function re(se,ie){const o=d("ExternalLinkIcon");return l(),h("div",null,[_,u(" more "),w,e("p",null,[t("We did a lot of searching for alternative platforms and ultimately settled on using GitHub actions for nearly everything. This isn't perfect; for example, we cannot use it for our Apple silicon (M1, M2) builds, nor for ARM Linux, for which we are still using Cirrus CI. Over the next week there was a huge amount of effort put in by a number of Pulsar team members, but particularly "),e("a",y,[t("@confused-techie"),a(o)]),t(", "),e("a",k,[t("@DeeDeeG"),a(o)]),t(" and "),e("a",v,[t("@Meadowsys"),a(o)]),t(" to get everything moved and migrated in order to keep our builds building and our rolling releases rolling.")]),x,T,C,P,e("p",null,[t("If you use the Chocolately package manager for Windows, you may have noticed the official packages have been a few versions behind. This has now been solved and the latest versions are available once again with a lot of improvements to the process to avoid this kind of thing ever happening again. A huge thanks to "),e("a",A,[t("@COLAMAroro"),a(o)]),t(", "),e("a",I,[t("@spiker985"),a(o)]),t(" and "),e("a",W,[t("@confused-techie"),a(o)]),t(" for implementing this. We have already put up a whole blog post for this, so if you missed it, you can "),e("a",S,[t("read it here"),a(o)]),t(".")]),j,e("p",null,[e("a",M,[t("@confused-techie"),a(o)]),t(" has recently added a "),e("a",D,[t("new feature"),a(o)]),t(" to allow some additional configuration of Pulsar's title bar data. This new settings option allows you to decide if you want the current active Pulsar tab to be prepended to the title bar or not. This can potentially aid in readability for some, particularly if you are somebody who likes to work with multiple windows.")]),N,U,L,E,e("p",null,[t("This is currently available in our "),e("a",V,[t("rolling release"),a(o)]),t(" and will be in our next regular release in a couple of weeks.")]),q,O,e("p",null,[t("There are some very good reasons to want to unpublish a package or a specific version from the Pulsar Package Repository. You may have made a mistake or simply want to deprecate an old, non-functional version. Either way, we had an issue; it just wasn't working. Thankfully, due to some great collaboration between "),e("a",R,[t("@asiloisad/@bacadra"),a(o)]),t(" and "),e("a",B,[t("@confused-techie"),a(o)]),t(", the problem was "),e("a",G,[t("found and patched"),a(o)]),t(".")]),H,Y,e("p",null,[t("So with the above process and an improvement to the wording in the "),z,t(" command coming soon, we hope that this situation can be avoided in the future. A full writeup of what was done has been added to our "),e("a",$,[t("admin actions log"),a(o)]),t(" that details events just like this for full transparency to the community.")]),F,J,e("p",null,[t("It was brought to our attention by a community member that, upon uninstall, Pulsar was not clearing up all the directories it created during installation. Whilst this is somewhat expected for the configuration directory, there were also examples of other elements being left behind. To this end, "),e("a",K,[t("@confused-techie"),a(o)]),t(" has been putting together a new package, "),e("a",Q,[t("pulsar-cleanup"),a(o)]),t(", to try and deal with these leftover elements. As "),X,t(" is largely in control of the uninstall process, some elements do get left over at the end of the process. This utility will be a stand-alone executable that can be used to fully clean up all these extra artifacts, leaving your system entirely clean of any Pulsar installation.")]),Z,ee,e("p",null,[t("In our community spotlight this month, we want to say a big thank you to new first time contributor "),e("a",te,[t("@casswedson"),a(o)]),t(" for "),e("a",oe,[t("this PR"),a(o)]),t(" to remove a bunch of deprecation warnings in one of our workflows.")]),ae,ne])}const de=i(b,[["render",re],["__file","20230904-Daeraxa-SeptemberUpdate.html.vue"]]);export{de as default}; diff --git a/assets/20230904-Daeraxa-SeptemberUpdate.html.77d3175b.js b/assets/20230904-Daeraxa-SeptemberUpdate.html.77d3175b.js new file mode 100644 index 0000000000..a3d8aa4d82 --- /dev/null +++ b/assets/20230904-Daeraxa-SeptemberUpdate.html.77d3175b.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-4dac8919","path":"/blog/20230904-Daeraxa-SeptemberUpdate.html","title":"Community Update","lang":"en-US","frontmatter":{"title":"Community Update","author":"Daeraxa","date":"2023-09-04T00:00:00.000Z","category":["news","log"],"tag":["update"]},"excerpt":"I want you to act as a Pulsar team member writing a blog post. I will provide titles of recent changes. You will elaborate on each topic and make each paragraph coherent from my ramblings. You will welcome all readers to the September Pulsar community update.
\\n","headers":[{"level":2,"title":"Upheaving our CI setup","slug":"upheaving-our-ci-setup","link":"#upheaving-our-ci-setup","children":[]},{"level":2,"title":"Chocolately packages are up to date again!","slug":"chocolately-packages-are-up-to-date-again","link":"#chocolately-packages-are-up-to-date-again","children":[]},{"level":2,"title":"New title bar configuration option","slug":"new-title-bar-configuration-option","link":"#new-title-bar-configuration-option","children":[]},{"level":2,"title":"Unpublishing packages","slug":"unpublishing-packages","link":"#unpublishing-packages","children":[]},{"level":2,"title":"Pulsar cleanup","slug":"pulsar-cleanup","link":"#pulsar-cleanup","children":[]},{"level":2,"title":"Community spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[]}],"git":{"updatedTime":1693877975000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4}]},"readingTime":{"minutes":4.34,"words":1303},"filePathRelative":"blog/20230904-Daeraxa-SeptemberUpdate.md","localizedDate":"September 4, 2023"}');export{e as data}; diff --git a/assets/20230916-Daeraxa-v1.109.0.html.6de34209.js b/assets/20230916-Daeraxa-v1.109.0.html.6de34209.js new file mode 100644 index 0000000000..d843461b50 --- /dev/null +++ b/assets/20230916-Daeraxa-v1.109.0.html.6de34209.js @@ -0,0 +1 @@ +import{_ as a,o as n,c as s,a as t,b as e,d as r,e as l,f as i,r as u}from"./app.0e1565ce.js";const h={},d={href:"https://github.com/pulsar-edit/pulsar/releases/tag/v1.109.0",target:"_blank",rel:"noopener noreferrer"},c=t("h2",{id:"what-do-we-have-for-you-in-pulsar-1-109-0",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#what-do-we-have-for-you-in-pulsar-1-109-0","aria-hidden":"true"},"#"),e(" What do we have for you in Pulsar 1.109.0?")],-1),p=t("p",null,"Our next release has arrived, and, as ever, we are excited to share all the changes we have been making over the last month since our last release.",-1),f=t("p",null,[e('We have a smorgasbord of bug fixes and quality of life improvements this month that we hope will make things just that bit better overall. Many of these changes are "behind the scenes", so you may not see much evidence of them when using Pulsar, but the '),t("em",null,"lack"),e(" of any obvious changes means that we actually accomplished our goals of not disrupting the user experience.")],-1),g={href:"https://pulsar-edit.dev/blog/20230903-confused-Techie-pulsars-ci.html",target:"_blank",rel:"noopener noreferrer"},m=t("p",null,`To continue the trend of "background" changes, we have finally achieved our goal of removing all of the CoffeeScript code in the core editor and packages (a process dubbed "decaffeination") in favour of standard JavaScript. This was a goal started by the Atom team and has been a long time coming as, whilst it was good at the time, many of the features of CoffeeScript can now be found in vanilla JavaScript and migrating away from it allows a more unified codebase that is easier to maintain and lowers the barrier of entry for new contributors. Again, you won't really see anything within Pulsar itself, but this was a big achievement and we can finally draw a close to this little chapter.`,-1),_=t("em",null,"will",-1),b=t("code",null,"autoUpdate",-1),w=t("code",null,"1.108.0",-1),k=t("code",null,"pulsar-updater",-1),v={href:"https://pulsar-edit.dev/blog/20230816-DeeDeeG-v1.108.0.html#what-is-new-in-1-108-0",target:"_blank",rel:"noopener noreferrer"},y=t("code",null,"about",-1),x=t("code",null,"pulsar-updater",-1),A=t("p",null,[e("On the topic of the "),t("code",null,"about"),e(" package, we have some nice quality of life changes. The first of which is a change to how we link to the changelog/release notes. Previously, this would link to the changelog state as it was at that specific release. We have changed this to instead link to the relevant section on our "),t("code",null,"master"),e(" branch changelog. This not only allows us to properly support our rolling releases but also gives our regular releases quick access to look at upcoming changes. We also have a nice little update to improve the responsiveness of the pane when displayed in a narrow format; no longer do things get shoved and squished out of place.")],-1),T=t("p",null,[e("Now onto some bug fixes. The first one here regards a race condition that was found in our "),t("code",null,"autocomplete-plus"),e(" package, which was causing some weird situations where the suggestion list would open, close, then open again. This has now been fixed (with some handy assistance from the core package "),t("code",null,"keyboard-resolver"),e(" to help narrow down the problem).")],-1),P=t("p",null,"The next is specific to Windows users, where the Pulsar logo was not being shown (and was being replaced with a default blank icon) when Pulsar was set to be the file handler for selected file types. This has now been solved, and the logo should be displaying correctly.",-1),D=t("code",null,"settings-view",-1),C={href:"https://pulsar-edit.dev/download.html#rolling-release",target:"_blank",rel:"noopener noreferrer"},S={href:"https://opencollective.com/pulsar-edit",target:"_blank",rel:"noopener noreferrer"},R=i('And that just about draws things to a close for this release. We have had a particularly busy month with some rather significant changes to all kinds of areas of Pulsar, so we hope you enjoy the update. As ever, a huge thank you to our generous donors and community, without whom this project would not be possible.
Until next time, happy coding, and see you amongst the stars!
autocomplete-plus
to ignore user input.about
package linking to release notes for Pulsar.settings-view
creates.autoUpdate
API from Pulsar, instead relying on the pulsar-updater
package.Going the whole nine yards: Get Pulsar 1.109.0
Join !pulsaredit@lemmy.ml!
\\n","headers":[{"level":2,"title":"Come and join our newly opened Lemmy community!","slug":"come-and-join-our-newly-opened-lemmy-community","link":"#come-and-join-our-newly-opened-lemmy-community","children":[]}],"git":{"updatedTime":1694883547000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.97,"words":290},"filePathRelative":"blog/20230917-Daeraxa-LemmyCommunity.md","localizedDate":"September 17, 2023"}');export{e as data}; diff --git a/assets/20230917-Daeraxa-LemmyCommunity.html.b23f8cd4.js b/assets/20230917-Daeraxa-LemmyCommunity.html.b23f8cd4.js new file mode 100644 index 0000000000..2a1a06e991 --- /dev/null +++ b/assets/20230917-Daeraxa-LemmyCommunity.html.b23f8cd4.js @@ -0,0 +1 @@ +import{_ as a,o as i,c as r,e as m,a as e,b as t,d as n,r as s}from"./app.0e1565ce.js";const c="/assets/pulsar-lemmy.7d5967b0.png",l={},u=e("p",null,"Join !pulsaredit@lemmy.ml!",-1),d=e("h2",{id:"come-and-join-our-newly-opened-lemmy-community",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#come-and-join-our-newly-opened-lemmy-community","aria-hidden":"true"},"#"),t(" Come and join our newly opened Lemmy community!")],-1),h=e("a",{href:"https://lemmy.ml/c/pulsaredit"},[e("img",{src:c,height:"300",class:"blog-image-center",alt:"Lemmy community summary"})],-1),y=e("p",null,"During the Reddit blackout a couple of months ago, we had a lot of interest in the creation of an official Lemmy community in combination with our Subreddit. After checking things again with a vote, we have decided to officially open our Lemmy instance.",-1),p={href:"https://lemmy.ml/c/pulsaredit",target:"_blank",rel:"noopener noreferrer"},f={href:"https://fosstodon.org/@pulsaredit",target:"_blank",rel:"noopener noreferrer"},_=e("em",null,"need",-1),g=e("code",null,"pulsaredit",-1),b={href:"https://join-lemmy.org/instances",target:"_blank",rel:"noopener noreferrer"},w=e("p",null,[t("Once you have made your Lemmy account (or already have one), you can join the community by searching for "),e("code",null,"!pulsaredit@lemmy.ml"),t(" from within your instance's search, clicking the community name, and hitting "),e("code",null,"Subscribe"),t(" in the top right.")],-1);function k(L,x){const o=s("ExternalLinkIcon");return i(),r("div",null,[u,m(" more "),d,h,y,e("p",null,[t("So if you like the Reddit style format but would prefer federation and open source software, then come and join us at "),e("a",p,[t("pulsaredit@lemmy.ml"),n(o)]),t("!")]),e("p",null,[t("With this, we expand upon our existing presence in the fediverse ("),e("a",f,[t("@pulsaredit@fosstodon.org"),n(o)]),t(") and hope that we can grow the community in these spaces.")]),e("p",null,[t("If you are new to Lemmy or the fediverse in general, it is important to understand that you don't "),_,t(" to join the instance that the community is hosted on. In the case of Lemmy, our instance is on https://lemmy.ml/, but you can join pretty much any community you like and still fully interact with the "),g,t(" community - this is what federation is all about. If you don't have a Lemmy account, then you can go to "),e("a",b,[t("https://join-lemmy.org/"),n(o)]),t(" to help you find a community that caters to your interests.")]),w])}const j=a(l,[["render",k],["__file","20230917-Daeraxa-LemmyCommunity.html.vue"]]);export{j as default}; diff --git a/assets/20230925-savetheclocktower-modern-tree-sitter-part-1.html.3d8130c9.js b/assets/20230925-savetheclocktower-modern-tree-sitter-part-1.html.3d8130c9.js new file mode 100644 index 0000000000..cd906467f0 --- /dev/null +++ b/assets/20230925-savetheclocktower-modern-tree-sitter-part-1.html.3d8130c9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-25bd77f6","path":"/blog/20230925-savetheclocktower-modern-tree-sitter-part-1.html","title":"Modern Tree-sitter, part 1: the new old feature","lang":"en-US","frontmatter":{"title":"Modern Tree-sitter, part 1: the new old feature","author":"savetheclocktower","date":"2023-09-25T00:00:00.000Z","category":["dev"],"tag":["modernization","tree-sitter"]},"excerpt":"The last few releases of Pulsar have been bragging about a feature that arguably isn\u2019t even new: our experimental \u201Cmodern\u201D Tree-sitter implementation. You might\u2019ve read that phrase a few times now without fully understanding what it means, and an explanation is long overdue.
\\n","headers":[{"level":2,"title":"What is Tree-sitter?","slug":"what-is-tree-sitter","link":"#what-is-tree-sitter","children":[]},{"level":2,"title":"What is the new Tree-sitter integration replacing?","slug":"what-is-the-new-tree-sitter-integration-replacing","link":"#what-is-the-new-tree-sitter-integration-replacing","children":[]},{"level":2,"title":"If Tree-sitter is already in Pulsar, why write a new implementation?","slug":"if-tree-sitter-is-already-in-pulsar-why-write-a-new-implementation","link":"#if-tree-sitter-is-already-in-pulsar-why-write-a-new-implementation","children":[]},{"level":2,"title":"Why is Tree-sitter better in general?","slug":"why-is-tree-sitter-better-in-general","link":"#why-is-tree-sitter-better-in-general","children":[]},{"level":2,"title":"Why is this new implementation better than the old one?","slug":"why-is-this-new-implementation-better-than-the-old-one","link":"#why-is-this-new-implementation-better-than-the-old-one","children":[]},{"level":2,"title":"I disabled Tree-sitter grammars at some point, and I don\u2019t feel like I\u2019ve missed anything. Why should I turn them back on?","slug":"i-disabled-tree-sitter-grammars-at-some-point-and-i-don-t-feel-like-i-ve-missed-anything-why-should-i-turn-them-back-on","link":"#i-disabled-tree-sitter-grammars-at-some-point-and-i-don-t-feel-like-i-ve-missed-anything-why-should-i-turn-them-back-on","children":[]},{"level":2,"title":"Can I use this new implementation now?","slug":"can-i-use-this-new-implementation-now","link":"#can-i-use-this-new-implementation-now","children":[]},{"level":2,"title":"Which Tree-sitter grammars come with Pulsar?","slug":"which-tree-sitter-grammars-come-with-pulsar","link":"#which-tree-sitter-grammars-come-with-pulsar","children":[]},{"level":2,"title":"The old grammar highlighted my code in a way that I liked. Now things are colored differently and it\u2019s driving me nuts. Should I turn off Tree-sitter?","slug":"the-old-grammar-highlighted-my-code-in-a-way-that-i-liked-now-things-are-colored-differently-and-it-s-driving-me-nuts-should-i-turn-off-tree-sitter","link":"#the-old-grammar-highlighted-my-code-in-a-way-that-i-liked-now-things-are-colored-differently-and-it-s-driving-me-nuts-should-i-turn-off-tree-sitter","children":[]},{"level":2,"title":"Why should I write a Tree-sitter grammar for Pulsar?","slug":"why-should-i-write-a-tree-sitter-grammar-for-pulsar","link":"#why-should-i-write-a-tree-sitter-grammar-for-pulsar","children":[]},{"level":2,"title":"Why is this interesting enough to write about?","slug":"why-is-this-interesting-enough-to-write-about","link":"#why-is-this-interesting-enough-to-write-about","children":[]}],"git":{"updatedTime":1695678390000,"contributors":[{"name":"Andrew Dupont","email":"github@andrewdupont.net","commits":1}]},"readingTime":{"minutes":8.62,"words":2585},"filePathRelative":"blog/20230925-savetheclocktower-modern-tree-sitter-part-1.md","localizedDate":"September 25, 2023"}');export{e as data}; diff --git a/assets/20230925-savetheclocktower-modern-tree-sitter-part-1.html.a328c2a8.js b/assets/20230925-savetheclocktower-modern-tree-sitter-part-1.html.a328c2a8.js new file mode 100644 index 0000000000..b7f47790f4 --- /dev/null +++ b/assets/20230925-savetheclocktower-modern-tree-sitter-part-1.html.a328c2a8.js @@ -0,0 +1 @@ +import{_ as n,o,c as s,e as l,a as e,b as t,d as a,f as i,r as h}from"./app.0e1565ce.js";const d={},u=e("p",null,[t("The last few releases of Pulsar have been bragging about a feature that arguably isn\u2019t even new: our "),e("strong",null,"experimental \u201Cmodern\u201D Tree-sitter implementation"),t(". You might\u2019ve read that phrase a few times now without fully understanding what it means, and an explanation is long overdue.")],-1),c=e("p",null,"This is the first of a series of articles about Pulsar\u2019s ongoing project to migrate its Tree-sitter implementation to a more modern version \u2014\xA0the culmination of hundreds of hours of development work that started back in February of this year. It first shipped in Pulsar version 1.106 back in June as an opt-in feature, and is being improved on an ongoing basis with each new monthly release.",-1),m=e("p",null,"This is a big feature, perhaps the biggest since Pulsar was forked from Atom \u2014 and yet it\u2019s a feature that, if we\u2019ve done our jobs right, won\u2019t even seem like much of a change to most users. Before we dive into the deep end, I\u2019ll try to explain why this is a topic worthy of multiple blog posts.",-1),g=e("h2",{id:"what-is-tree-sitter",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#what-is-tree-sitter","aria-hidden":"true"},"#"),t(" What is Tree-sitter?")],-1),p={href:"https://tree-sitter.github.io/tree-sitter/",target:"_blank",rel:"noopener noreferrer"},f={href:"https://github.com/maxbrunsfeld",target:"_blank",rel:"noopener noreferrer"},y={href:"https://zed.dev/",target:"_blank",rel:"noopener noreferrer"},w=i('It\u2019s a code parsing system that represents your code as a tree of nodes. It\u2019s very fast on first parse \u2014 and even faster at re-parsing code after you\u2019ve made changes, because it can reuse the output from the last parse and reprocess only the parts that have changed.
You can use its output to underpin lots of features that you\u2019d need in a code editor:
A Tree-sitter parser is designed to parse code quickly, but not necessarily with 100% accuracy; the goal is to be accurate enough for the purposes listed above.
The new Tree-sitter integration \u2014 which I\u2019ll be calling modern Tree-sitter throughout this series \u2014 won\u2019t replace anything except for the previous Tree-sitter integration, which I\u2019ll be calling legacy Tree-sitter.
Once we decide modern Tree-sitter is stable, we\u2019ll drop support for legacy Tree-sitter so that Pulsar can update to a newer version of Electron.
',7),b={href:"https://macromates.com/manual/en/language_grammars",target:"_blank",rel:"noopener noreferrer"},_=e("strong",null,"TextMate grammars",-1),T={href:"https://macromates.com/",target:"_blank",rel:"noopener noreferrer"},v={href:"https://code.visualstudio.com/",target:"_blank",rel:"noopener noreferrer"},k={href:"https://www.sublimetext.com/",target:"_blank",rel:"noopener noreferrer"},x=e("h2",{id:"if-tree-sitter-is-already-in-pulsar-why-write-a-new-implementation",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#if-tree-sitter-is-already-in-pulsar-why-write-a-new-implementation","aria-hidden":"true"},"#"),t(" If Tree-sitter is already in Pulsar, why write a new implementation?")],-1),I=e("p",null,"Good question! Atom was, after all, the first code editor to ship with support for Tree-sitter. It was introduced in late 2017, and was made the preferred system for syntax highlighting starting with Atom 1.32 nearly a year later.",-1),M=e("p",null,"There are two major reasons why the legacy implementation needs to be replaced:",-1),P=e("p",null,[e("strong",null,"Tree-sitter now has powerful features that the legacy implementation doesn\u2019t leverage."),t(" As is often the case, being the first to implement it meant that Atom found all of Tree-sitter\u2019s early pain points. It was a stated goal to use TextMate-style scope names in the new Tree-sitter grammars \u2014\xA0so as to make migration easier \u2014 but Atom had to invent its own system for mapping Tree-sitter output to scope names, and that system didn\u2019t have the flexibility it needed to match TextMate grammars\u2019 syntax highlighting in all cases. This revealed a need for a more robust system of describing tree nodes, and for highlighting ranges that didn\u2019t correspond to the exact ranges of tree nodes.")],-1),S={href:"https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries",target:"_blank",rel:"noopener noreferrer"},A=e("p",null,"That\u2019s a task worth doing, but it will change how Tree-sitter grammars are written, so there\u2019s no way to avoid the fact that backward compatibility will be broken. But this is a perfect time to make the leap, because\u2026",-1),j=e("strong",null,[t("We need to switch to the "),e("code",null,"web-tree-sitter"),t(" bindings.")],-1),B={href:"https://nodejs.github.io/node-addon-examples/special-topics/context-awareness/",target:"_blank",rel:"noopener noreferrer"},C=e("code",null,"node-tree-sitter",-1),W=e("code",null,"node-tree-sitter",-1),E={href:"https://github.com/tree-sitter/tree-sitter/tree/master/lib/binding_web",target:"_blank",rel:"noopener noreferrer"},L=e("code",null,"web-tree-sitter",-1),N={href:"https://webassembly.org/",target:"_blank",rel:"noopener noreferrer"},O=e("code",null,"node-tree-sitter",-1),R=e("code",null,"web-tree-sitter",-1),V=e("code",null,"node-tree-sitter",-1),q=e("p",null,[t("If, someday, the "),e("code",null,"node-tree-sitter"),t(" bindings were updated to be easier to use in an Electron context, we\u2019d be able to migrate back without any further loss of backward compatibility. But for now, "),e("code",null,"web-tree-sitter"),t(" is the way forward, and we\u2019re pleasantly surprised at how well it does the job.")],-1),U=i('Nobody likes to break backward compatibility, but needing to switch to web-tree-sitter
presents us with an opportunity. Tree-sitter is more stable and more robust than it was in 2017, so we\u2019re able to replace legacy Tree-sitter with something better rather than something that\u2019s merely equivalent.
Here are a few reasons why Pulsar is using Tree-sitter at all, and why Pulsar is configured to prefer a Tree-sitter grammar over a TextMate grammar when both are present:
The specific ways in which Tree-sitter will make your life easier will vary based on which languages you use most often, but this post series will explore a handful of examples.
An under-the-hood change like this isn\u2019t necessarily something you\u2019d notice. But Pulsar users may notice some of the downstream effects:
The benefits are much more direct to grammar authors:
TextMate grammars are still the main style of grammar in Visual Studio Code, Sublime Text, and other editors. They can\u2019t do all the things that Tree-sitter parsers can do, and most new editors on the market have chosen to use Tree-sitter instead; but even just VSCode\u2019s example tells us that TextMate grammars are no impediment to having a popular and feature-filled editor.
So I\u2019ll be clear: we have no plans to deprecate TextMate-style grammars. They still have their place in Pulsar, and the only thing we\u2019d achieve by deprecating them is to disrupt the editor experience of many of our users.
In the future, it will still be possible (as it is today) to turn off Tree-sitter grammars, either altogether or selectively for certain kinds of files, and fall back to a TextMate grammar for a given language (if it exists).
But our hope is that you\u2019ll give this new Tree-sitter system a chance, even if you\u2019d disabled Tree-sitter grammars in the past for any reason. We think it\u2019s got all the upsides of the legacy Tree-sitter integration without any of the downsides.
Yes, you can, as long as you\u2019re on Pulsar 1.106 or greater. Open your Pulsar settings and focus the \u201CCore\u201D pane. Find the setting named Use Modern Tree-Sitter Implementation and make sure it\u2019s checked, then make sure that the nearby setting named Use Tree-Sitter Parsers is also checked. Then restart Pulsar or reload your window.
If you routinely use the grammar selector and want to be able to switch between Tree-sitter grammars and TextMate grammars at will, locate the grammar-selector
package in the \u201CPackages\u201D pane, then click on its Settings button. Uncheck the setting named Hide Duplicate TextMate Grammars. This will give you the ability to choose between modern Tree-sitter, legacy Tree-sitter, and TextMate grammars.
Currently, these grammars are built in:
In addition, Pulsar ships with several specialty Tree-sitter parsers that can be injected into other grammars:
If you use a language that isn\u2019t on the list above and you\u2019re curious about what it would take to give that language a Tree-sitter grammar, you\u2019ll get extra value out of this post series.
Please don\u2019t! It\u2019d be like amputating your finger to get rid of a hangnail.
',26),Y={href:"https://github.com/orgs/pulsar-edit/discussions",target:"_blank",rel:"noopener noreferrer"},H=e("h2",{id:"why-should-i-write-a-tree-sitter-grammar-for-pulsar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#why-should-i-write-a-tree-sitter-grammar-for-pulsar","aria-hidden":"true"},"#"),t(" Why should I write a Tree-sitter grammar for Pulsar?")],-1),J=e("p",null,"Because it\u2019s a much friendlier experience than writing your own TextMate grammar, provided that a Tree-sitter parser exists for the language in question.",-1),G=e("code",null,"nvim-treesitter",-1),F={href:"https://github.com/nvim-treesitter/nvim-treesitter#supported-languages",target:"_blank",rel:"noopener noreferrer"},z=e("p",null,"In my experience, turning a Tree-sitter parser into a full-fledged Pulsar grammar takes less than two hours.",-1),D=e("h2",{id:"why-is-this-interesting-enough-to-write-about",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#why-is-this-interesting-enough-to-write-about","aria-hidden":"true"},"#"),t(" Why is this interesting enough to write about?")],-1),Z=e("p",null,"This Tree-sitter overhaul is the biggest feature to be introduced to Pulsar since it was forked from Atom, and it\u2019s a feature that covers a lot of the surface area of the core editing experience.",-1),X={href:"https://zed.dev/",target:"_blank",rel:"noopener noreferrer"},K={href:"https://nova.app/",target:"_blank",rel:"noopener noreferrer"},Q={href:"https://lapce.dev/",target:"_blank",rel:"noopener noreferrer"},$={href:"https://en.wikipedia.org/wiki/Greenfield_project",target:"_blank",rel:"noopener noreferrer"},ee=e("p",null,[t("But we\u2019ve got a harder job. Atom embraced most of the concepts inherent to TextMate grammars and built major editor features around them. It wouldn\u2019t be very user-friendly if we introduced a parallel system with a different set of concepts \u2014 it would force users to be aware of "),e("em",null,"which kind"),t(" of language grammar they\u2019re using, and to juggle their mental model accordingly.")],-1),te=e("p",null,"But also: most Pulsar users rely on at least a few community packages that were written for Atom and aren\u2019t actively maintained. We have to be very careful to break backward compatibility as little as possible, and only when it\u2019s absolutely necessary.",-1),re=e("p",null,"For these reasons, we shouldn\u2019t just introduce brand new systems for code highlighting, contextual awareness, and the rest. Instead, we\u2019ll do whatever we can to make the new Tree-sitter system work within \u2014 or identically to \u2014 systems that Atom originally shipped with. The Tree-sitter integration can offer enhancements beyond what TextMate grammars do \u2014 and it will! \u2014 but it\u2019s still got to live in the world that TextMate grammars created.",-1),ae=e("em",null,"conventions",-1),ie={href:"https://github.com/nvim-treesitter/nvim-treesitter",target:"_blank",rel:"noopener noreferrer"},ne=e("p",null,"If you\u2019re at all interested in how we did it, stay tuned for the rest of this series.",-1);function oe(se,le){const r=h("ExternalLinkIcon");return o(),s("div",null,[u,l(" more "),c,m,g,e("p",null,[e("a",p,[t("Tree-sitter"),a(r)]),t(" is a code parsing system. It\u2019s the brainchild of "),e("a",f,[t("Max Brunsfeld"),a(r)]),t(", current "),e("a",y,[t("Zed"),a(r)]),t(" contributor and former contributor to Atom.")]),w,e("p",null,[t("Tree-sitter will continue to exist alongside Atom\u2019s original system for syntax highlighting: "),e("a",b,[_,a(r)]),t(". This grammar system is based on the one invented by "),e("a",T,[t("TextMate"),a(r)]),t(" many years ago, and it\u2019s still being used by editors like "),e("a",v,[t("Visual Studio Code"),a(r)]),t(" and "),e("a",k,[t("Sublime Text"),a(r)]),t(".")]),x,I,M,e("ol",null,[e("li",null,[P,e("p",null,[t("Tree-sitter eventually introduced a powerful "),e("a",S,[t("query language"),a(r)]),t(" that could make the job of syntax highlighting easier. But by that point, Microsoft had bought GitHub, and Atom seemed not to be a major priority anymore, so the legacy implementation was never updated to adopt this query language.")]),A]),e("li",null,[e("p",null,[j,t(" One of the goals of Pulsar is to be able to run the editor on the latest version of Electron. Unfortunately, newer Electron versions make it difficult for Pulsar to use Node modules that are not "),e("a",B,[t("context-aware"),a(r)]),t(". The legacy Tree-sitter implementation uses the "),C,t(" bindings, and it appears to be a tall task to adapt these bindings so that they can be used in newer Electron versions. Right now, Pulsar\u2019s reliance on "),W,t(" is preventing us from upgrading Electron to anything past our current version, 12.2.3 \u2014\xA0which is nearly two years old.")]),e("p",null,[t("So we decided to migrate to the "),e("a",E,[L,t(" bindings"),a(r)]),t(". They use "),e("a",N,[t("WebAssembly"),a(r)]),t(" and can run safely inside a browser or an Electron application. Using WebAssembly instead of a native C++ module like "),O,t(" involves a performance penalty, but we\u2019ve found that penalty to be very small in practice. The "),R,t(" bindings are robust and can do nearly everything that "),V,t(" can do.")]),q])]),U,e("p",null,[t("Instead, you can use your user stylesheet to apply a few lines of overrides to your syntax theme and restore the look you\u2019re used to. "),e("a",Y,[t("Open a topic in our discussion forums"),a(r)]),t(" and someone can tell you exactly how to do it.")]),H,J,e("p",null,[t("Pulsar already has built-in Tree-sitter grammars for most common programming languages. But if you\u2019re a consumer of something more obscure, you might find that someone\u2019s already written a parser for it. The "),G,t(" project \u2014 arguably the largest extant consumer of Tree-sitter \u2014 is responsible for the creation of "),e("a",F,[t("dozens of Tree-sitter parsers"),a(r)]),t(" for niche languages.")]),z,D,Z,e("p",null,[t("Other Tree-sitter\u2013integrated editors like "),e("a",X,[t("Zed"),a(r)]),t(", "),e("a",K,[t("Nova"),a(r)]),t(", and "),e("a",Q,[t("Lapce"),a(r)]),t(" are, to the best of my knowledge, "),e("a",$,[t("greenfield projects"),a(r)]),t(". They are free to invent entirely new conventions.")]),ee,te,re,e("p",null,[t("So in order to pull this off \u2014 to make modern Tree-sitter grammars work within existing systems \u2014 we had to create a brand new set of "),ae,t(" for writing Tree-sitter grammars. In some places, there was prior art from implementations like "),e("a",ie,[t("neovim"),a(r)]),t("\u2019s; in others we were flying blind and had to invent things from scratch.")]),ne])}const de=n(d,[["render",oe],["__file","20230925-savetheclocktower-modern-tree-sitter-part-1.html.vue"]]);export{de as default}; diff --git a/assets/20230927-savetheclocktower-modern-tree-sitter-part-2.html.1843c64c.js b/assets/20230927-savetheclocktower-modern-tree-sitter-part-2.html.1843c64c.js new file mode 100644 index 0000000000..05afd4de9f --- /dev/null +++ b/assets/20230927-savetheclocktower-modern-tree-sitter-part-2.html.1843c64c.js @@ -0,0 +1,41 @@ +import{_ as r}from"./tree-sitter-string-scopes-diagram.2db37c5c.js";import{_ as l,o as c,c as p,a as e,b as t,d as s,w as u,e as d,f as a,r as o}from"./app.0e1565ce.js";const h="/assets/tree-sitter-log-cursor-scope.1a459f7a.png",m={},g=e("p",null,"Today I\u2019ll try to illustrate what that system looks like and why it\u2019s important.",-1),f={href:"https://github.com/mauricioszabo/",target:"_blank",rel:"noopener noreferrer"},b=e("code",null,"web-tree-sitter",-1),k={href:"https://github.com/tree-sitter/tree-sitter-javascript/blob/master/queries/highlights.scm",target:"_blank",rel:"noopener noreferrer"},y=e("code",null,"highlights.scm",-1),w={href:"https://tree-sitter.github.io/tree-sitter/syntax-highlighting",target:"_blank",rel:"noopener noreferrer"},v=e("code",null,"tree-sitter highlight",-1),q=e("p",null,[e("em",null,"Couldn\u2019t we just use these files and save ourselves a lot of hassle?"),t(", he asked.")],-1),_=e("p",null,"I said no. And I realized that, in saying no, I was volunteering myself to do some hard work.",-1),x=e("h2",{id:"how-textmate-grammars-work",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#how-textmate-grammars-work","aria-hidden":"true"},"#"),t(" How TextMate grammars work")],-1),T={href:"https://macromates.com/",target:"_blank",rel:"noopener noreferrer"},j={href:"https://www.sublimetext.com/",target:"_blank",rel:"noopener noreferrer"},I={href:"https://code.visualstudio.com/",target:"_blank",rel:"noopener noreferrer"},M={href:"https://en.wikipedia.org/wiki/TextMate#Language_Grammars",target:"_blank",rel:"noopener noreferrer"},S=a(`Here\u2019s a very simple example of what a TextMate grammar looks like:
{
+ "name": "JavaScript",
+ "scopeName": "source.js",
+ "fileTypes": ["js", "jsx", "mjs", "cjs"],
+
+ "patterns": [
+ {
+ "name": "string.quoted.double.js",
+ "match": "\\\\\\"(.*?)\\\\\\""
+ }
+ ]
+}
+
It\u2019ll feel overwhelming to consider all of the things a grammar does, so for this example I\u2019ve removed all of its rules except for one: the one that highlights double-quoted strings "like this"
. You can see that it looks for a particular pattern and, when it matches, assigns the name string.quoted.double.js
to that range. That\u2019s called a scope name.
When you load a JavaScript file in Pulsar \u2014 or VSCode, or Sublime Text \u2014 this grammar will find all your strings and mark them with scope names. In turn, your editor\u2019s syntax theme will hook into that scope name to make that string a special color. (It will most likely target string
, rather than something more specific, but that\u2019s why scope names are divided into segments; targeting string
will also match string.quoted.double.js
.)
Let\u2019s make our grammar a bit more complex:
{
+ "name": "JavaScript",
+ "scopeName": "source.js",
+ "fileTypes": ["js", "jsx", "mjs", "cjs"],
+
+ "patterns": [
+ {
+ "name": "string.quoted.double.js",
+ "begin": "\\"",
+ "beginCaptures": {
+ "0": { "name": "punctuation.definition.string.begin.js" }
+ },
+ "end": "\\"",
+ "endCaptures": {
+ "0": { "name": "punctuation.definition.string.end.js" }
+ },
+ "patterns": [
+ {
+ "include": "#string_escapes"
+ }
+ ]
+ }
+ ]
+}
+
This is practically the exact rule for double-quoted strings from Pulsar\u2019s built-in TextMate grammar for JavaScript. It introduces some more advanced concepts:
begin
and end
patterns. If it can identify a buffer range that starts with a begin
match and ends with an end
match, it can scope the entire range that way, and can create a new context in which only certain patterns are applied \u2014 in this case, escape sequences.string.quoted.double.js
, but now we\u2019re also applying punctuation
scope names to the quote characters themselves.Here\u2019s a better way to visualize this:
If you were to put your cursor anywhere within a buffer and run the Editor: Log Cursor Scope command, you\u2019d see a list of scope names that are active at that buffer position, moving from broadest to narrowest:
Still, you\u2019d be forgiven if you felt like this was overkill. All we\u2019re doing here is applying some syntax highlighting, right? Do you need this much information just to make a string green?
TextMate has a good reason for wanting such verbose scope names, and for sprinkling them so liberally throughout your source code: they aren\u2019t used just for syntax highlighting.
It\u2019s better to think of scope names as intelligent annotations for your source code. The idea isn\u2019t that you\u2019ll want to make single-quoted strings a different color from double-quoted strings \u2014 though you could! \u2014\xA0but rather that other editor features could benefit from knowing whether you were inside a string, and what kind of string you were inside.
Because in TextMate, most editor actions \u2014 commands, snippets, and macros \u2014 can be made contextually aware.
Let\u2019s use commands as an example. Here are some commands you can write in TextMate:
{}
, pressing Return should insert two newlines, put the cursor between the two newlines, and indent the cursor\u2019s line by one level.In fact, all of these are real examples that are enabled by default in TextMate. And because they target generic scope names that are shared across a number of grammars, they\u2019ll work in nearly any language.
Did you notice how our grammar file defined a scopeName
of source.js
? That\u2019s a root scope name, and it applies to the entire file. And it means that commands can restrict themselves to certain languages just like any other context.
In TextMate, file beautification commands canonically use the hotkey Ctrl-Shift-H. By applying the right scope selector for each of these three commands, you can assign them all to Ctrl-Shift-H, and they won\u2019t get in each other\u2019s way.
There are a number of reasons why TextMate isn\u2019t widely used anymore: it\u2019s a macOS-only editor, its releases are frustratingly sporadic, and it\u2019s never had support for crucial features like split editor panes. But TextMate was a major influence on how Atom was built.
It\u2019s not widely known, but Atom and Pulsar offer nearly as much flexibility with scope names as TextMate does:
Any snippet can be defined to run in an arbitrary scope context. For instance, you could write a snippet that will only expand if you\u2019re inside of a JavaScript block comment; if you try to invoke it elsewhere, it\u2019ll act as though that snippet doesn\u2019t exist.
Built-in packages use this system. For instance, the language-html
package defines snippets for most common tag names, but then redefines each one to do nothing if the cursor is inside a context where HTML would be invalid, like a style
or script
element.
Pulsar\u2019s configuration system allows for scope-specific overrides. For instance, a user can set editor.softWrap
to false
globally, but set it to true
for Markdown files.
Built-in packages use this system. Scopes can be used to set config values that aren\u2019t exposed in the UI \u2014 like what to use for comment delimiters in a given language, or which non-letter characters should be considered to be part of a \u201Cword\u201D for the purposes of cursor navigation.
In Pulsar, commands can be invoked anywhere, whether the user is in a text editor or not; hence there\u2019s no strong system for enforcing that commands be invoked only in certain scope contexts.
But it\u2019s possible! I\u2019ve got several commands in my init.js
whose only purpose is to inspect the scope list of the active text editor\u2019s cursor, then delegate to one of several other commands depending on where the cursor is. It\u2019s not as easy as it could be, but the bones are there.
But that\u2019s not all. If you use Pulsar, chances are very good that you use a package \u2014\xA0built-in or third-party \u2014 that inspects scope names for contextual information. Here are just a few of many examples:
',6),P=e("li",null,[e("code",null,"autocomplete-plus"),t(" inspects scopes to decide whether it should offer suggestions at the cursor; it declines to do so whenever any of the current scopes matches a user-configurable list. (This is how it decides "),e("em",null,"not"),t(" to offer suggestions if you\u2019re typing within a comment, for instance.)")],-1),J=e("li",null,[e("code",null,"autocomplete-css"),t(" decides which suggestions to offer based on the scope name of the closest token.")],-1),W=e("li",null,[e("code",null,"bracket-matcher"),t(" uses scope names to locate and highlight paired tokens like brackets and matching HTML tags.")],-1),F={href:"https://web.pulsar-edit.dev/packages/emmet",target:"_blank",rel:"noopener noreferrer"},V=e("code",null,"emmet",-1),R=e("h2",{id:"what-would-break",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#what-would-break","aria-hidden":"true"},"#"),t(" What would break?")],-1),z={href:"https://github.com/tree-sitter/tree-sitter-javascript/blob/master/queries/highlights.scm#L85-L88",target:"_blank",rel:"noopener noreferrer"},E=e("code",null,"tree-sitter-javascript/queries/highlights.scm",-1),U=a(`[
+ (string)
+ (template_string)
+] @string
+
This rule treats three different kinds of strings \u2014\xA0single-quoted, double-quoted, and backtick-quoted \u2014 identically. If we used this rule as written, we\u2019d be applying a scope name of string
to all JavaScript strings. Not string.quoted.double.js
and the like; just string
. And there\u2019s no rule in that file that applies scope names to string delimiters, either.
If we embraced a system that used names like string
and comment
instead of string.quoted.double.js
and comment.block.documentation.js
, the editing experience would be worse in a number of ways ways \u2014 some tiny, some large.
To drive it home for you, let\u2019s think of all the things that would break from this change alone:
punctuation
scopes would no longer be present on the delimiters.string.quoted.double
\u2014 or even string.quoted
\u2014 would break.punctuation
scope to detect whether the cursor is adjacent to a string delimiter would break.I think Atom learned this lesson rather well during the original rollout of Tree-sitter grammars. After updating, users were defaulted to the new grammars. Sometimes things looked different, or behaved differently. When users reported these differences, they learned that they could work around those bugs in the short term by unchecking the \u201CUse Tree-Sitter Grammars\u201D setting and restoring the original behavior. I wonder how many did, and how many of them ever re-enabled Tree-sitter grammars in the future.
A lot of code has been written that presumes the existence of TextMate-style scopes. I think this is fine. The fact that we have a new system doesn\u2019t mean that we should leave scopes behind. For one thing, it\u2019d be a jarring experience for users \u2014\xA0we\u2019d be bragging about a new feature that was likely to break customizations that users had been relying upon for years.
`,7),G={href:"https://en.wikipedia.org/wiki/Middleware",target:"_blank",rel:"noopener noreferrer"},D=a('So that\u2019s why I volunteered to do this. I knew that we couldn\u2019t just reuse some syntax highlighting query files that were written with different assumptions in place. And I knew that I couldn\u2019t reasonably ask someone else to do a bunch of work just because I felt like it was important.
My goal was to create a set of grammars that had the performance and accuracy upsides of the Tree-sitter experience and the rich scope name annotations that TextMate grammars provided.
For that to be possible, Tree-sitter grammars should apply scope names as similarly as possible to TextMate grammars. There may be reasons that we choose not to do something the way that the TextMate grammar does it, but we don\u2019t want to be unable to do something the way that the TextMate grammar does it.
Syntax highlighting was the first, and hardest, task. Next time I\u2019ll talk about how we solved it.
',5);function O(Y,K){const i=o("RouterLink"),n=o("ExternalLinkIcon");return c(),p("div",null,[e("p",null,[t("In "),s(i,{to:"/blog/20230925-savetheclocktower-modern-tree-sitter-part-1.html"},{default:u(()=>[t("the last post")]),_:1}),t(", I tried to explain why the new Tree-sitter integration was worth writing about in the first place: because we needed to integrate it into a system defined by TextMate grammars, and we had to solve some challenging problems along the way.")]),g,d(" more "),e("p",null,[t("When I first started getting involved with Pulsar back in January, "),e("a",f,[t("@mauricioszabo"),s(n)]),t(" had just begun the process of migrating us to the "),b,t(" bindings, and seemed to be dreading the enormity of the task.")]),e("p",null,[t("He noticed that most Tree-sitter parsers had "),e("a",k,[t("their own built-in query files"),s(n)]),t(" for syntax highlighting. These "),y,t(" files act like a sort of stylesheet for a Tree-sitter tree: they\u2019re used when you run "),e("a",w,[v,s(n)]),t(" from the command line, and they\u2019re what GitHub uses when it highlights code in a web browser.")]),q,_,x,e("p",null,[t("You may not have ever used "),e("a",T,[t("TextMate"),s(n)]),t(", but you\u2019ve probably used an editor that benefited from its existence. Several widely used editors \u2014 first "),e("a",j,[t("Sublime Text"),s(n)]),t(", then Atom, and now "),e("a",I,[t("Visual Studio Code"),s(n)]),t(" \u2014 all based their grammar systems on TextMate\u2019s.")]),e("p",null,[t("A "),e("a",M,[t("TextMate grammar"),s(n)]),t(" doesn\u2019t aim to understand your code entirely. It uses regular expressions \u2014 pattern-matching \u2014\xA0to describe rules that identify the important parts of your code. It knows that some rules apply broadly and others are valid only in certain contexts, so it allows you to nest rules inside other rules.")]),S,e("p",null,[t("Why is it choosing those specific names? Because of "),e("a",A,[t("naming conventions established by TextMate"),s(n)]),t(" many years ago. When different language grammars try to abide by the same conventions for naming their scopes, it makes it easier to write syntax themes without constantly checking how they look in every possible language.")]),H,e("p",null,[t("For instance: to beautify your JavaScript, you might want to run it through "),L,t(". To beautify your HTML, you might want to run it through "),e("a",C,[t("HTML Tidy"),s(n)]),t(". And to beautify your JSON, you might want to run it through "),B,t(".")]),N,e("ul",null,[P,J,W,e("li",null,[e("a",F,[V,s(n)]),t(", the eighth-most-downloaded community package, relies on scope descriptors to decide whether it should try to expand an abbreviation into HTML.")])]),R,e("p",null,[t("With this in mind, let\u2019s go back to those built-in Tree-sitter query files and consider our string example. In "),e("a",z,[E,s(n)]),t(" we can see how strings are treated:")]),U,e("p",null,[t("But I also think that TextMate-style scopes function as an elegant "),e("a",G,[t("middleware"),s(n)]),t(". They allow us to change an underlying implementation without the user needing to know or care \u2014 except, perhaps, to notice downstream benefits, like faster syntax highlighting and new editor features.")]),D])}const Z=l(m,[["render",O],["__file","20230927-savetheclocktower-modern-tree-sitter-part-2.html.vue"]]);export{Z as default}; diff --git a/assets/20230927-savetheclocktower-modern-tree-sitter-part-2.html.c9b9a8bd.js b/assets/20230927-savetheclocktower-modern-tree-sitter-part-2.html.c9b9a8bd.js new file mode 100644 index 0000000000..dcaa181f8e --- /dev/null +++ b/assets/20230927-savetheclocktower-modern-tree-sitter-part-2.html.c9b9a8bd.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5bad2e3c","path":"/blog/20230927-savetheclocktower-modern-tree-sitter-part-2.html","title":"Modern Tree-sitter, part 2: why scopes matter","lang":"en-US","frontmatter":{"title":"Modern Tree-sitter, part 2: why scopes matter","author":"savetheclocktower","date":"2023-09-27T00:00:00.000Z","category":["dev"],"tag":["modernization","tree-sitter"]},"excerpt":"In
Today I\u2019ll try to illustrate what that system looks like and why it\u2019s important.
\\n","headers":[{"level":2,"title":"How TextMate grammars work","slug":"how-textmate-grammars-work","link":"#how-textmate-grammars-work","children":[]},{"level":2,"title":"How TextMate uses scope names","slug":"how-textmate-uses-scope-names","link":"#how-textmate-uses-scope-names","children":[]},{"level":2,"title":"How Pulsar uses scope names","slug":"how-pulsar-uses-scope-names","link":"#how-pulsar-uses-scope-names","children":[]},{"level":2,"title":"What would break?","slug":"what-would-break","link":"#what-would-break","children":[]},{"level":2,"title":"The challenge","slug":"the-challenge","link":"#the-challenge","children":[]}],"git":{"updatedTime":1695833427000,"contributors":[{"name":"Andrew Dupont","email":"github@andrewdupont.net","commits":4}]},"readingTime":{"minutes":7.84,"words":2351},"filePathRelative":"blog/20230927-savetheclocktower-modern-tree-sitter-part-2.md","localizedDate":"September 27, 2023"}');export{e as data}; diff --git a/assets/20231004-Daeraxa-OctoberUpdate.html.3b1907f4.js b/assets/20231004-Daeraxa-OctoberUpdate.html.3b1907f4.js new file mode 100644 index 0000000000..8dacc0f7ce --- /dev/null +++ b/assets/20231004-Daeraxa-OctoberUpdate.html.3b1907f4.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-6cc4d458","path":"/blog/20231004-Daeraxa-OctoberUpdate.html","title":"Community Update","lang":"en-US","frontmatter":{"title":"Community Update","author":"Daeraxa","date":"2023-10-04T00:00:00.000Z","category":["news","log"],"tag":["update"]},"excerpt":"As the leaves turn brown and the days grow shorter, make sure you draw up a chair and settle in for a nice, warm, pumpkin spice edition of the Pulsar community update!
\\n","headers":[{"level":2,"title":"Introducing Pulsar Cooperative!","slug":"introducing-pulsar-cooperative","link":"#introducing-pulsar-cooperative","children":[]},{"level":2,"title":"Modern Tree-sitter blog posts","slug":"modern-tree-sitter-blog-posts","link":"#modern-tree-sitter-blog-posts","children":[]},{"level":2,"title":"Converting PPM's code from callbacks to async","slug":"converting-ppm-s-code-from-callbacks-to-async","link":"#converting-ppm-s-code-from-callbacks-to-async","children":[]},{"level":2,"title":"macOS binary signing issues","slug":"macos-binary-signing-issues","link":"#macos-binary-signing-issues","children":[]},{"level":2,"title":"Shields.io badges","slug":"shields-io-badges","link":"#shields-io-badges","children":[]},{"level":2,"title":"Pulsar on GitHub Desktop","slug":"pulsar-on-github-desktop","link":"#pulsar-on-github-desktop","children":[]},{"level":2,"title":"Community spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[]}],"git":{"updatedTime":1696805253000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":7}]},"readingTime":{"minutes":5.58,"words":1674},"filePathRelative":"blog/20231004-Daeraxa-OctoberUpdate.md","localizedDate":"October 4, 2023"}`);export{e as data}; diff --git a/assets/20231004-Daeraxa-OctoberUpdate.html.dbc8c526.js b/assets/20231004-Daeraxa-OctoberUpdate.html.dbc8c526.js new file mode 100644 index 0000000000..6cf2c1c9a2 --- /dev/null +++ b/assets/20231004-Daeraxa-OctoberUpdate.html.dbc8c526.js @@ -0,0 +1,56 @@ +import{_ as o}from"./tree-sitter.6a39e323.js";import{_ as i}from"./spotlight.aba19e76.js";import{_ as p,o as r,c,e as l,a,b as n,d as e,f as t,r as u}from"./app.0e1565ce.js";const d="/assets/community.aacaf0a9.png",h="/assets/development.9a152ab5.png",k="/assets/signing.c58cd4da.png",m="/assets/shields-io.b3bc3a32.png",g="/assets/github-desktop.a5a19b8f.png",b={},f=a("p",null,"As the leaves turn brown and the days grow shorter, make sure you draw up a chair and settle in for a nice, warm, pumpkin spice edition of the Pulsar community update!",-1),v=t('A warm welcome to this edition of our monthly blog post where we tell stories about what the Pulsar team and community have been up to in the world of Pulsar. This month we have a real mixed bag of things from an initial announcement of our "Pulsar Cooperative" initiative, a large code refactor of PPM, an explanation of some recent macOS signing issues and the appearance of Pulsar on Shields.io and GitHub Desktop! Enough with the prelude and let's get on with the update!
The problem is that some of the more popular packages are in this exact situation and we often see members of the community asking about an issue with a package, discovering the fix and implementing the fix locally, but for whatever reason the package maintainer no longer wishes to maintain that package with Pulsar in mind.
Of course, it is always possible to fork the package and take over maintenance yourself, but not everyone wants to, or may be in a position to, maintain a package. Nor does the core Pulsar team have the ability to maintain all of these community packages as well as the main Pulsar project.
So we have come up with what we hope will be a viable solution. Pulsar Cooperative. This is a new organization that we have set up to allow joint ownership of packages. The idea is thus:
We hope to introduce a place where people can cooperatively work to make the Pulsar ecosystem better without having to worry about violating the strict vision for a package and not having their fix even accepted. It is also a place where people can submit fixes and make their favourite packages functional again without necessarily having to take on full responsibility for the ongoing maintenance.
This is not a "Pulsar team" project in the sense that members of the main organization are going to be providing bug fixes or updates in response to logged issues (of course they are free too, just like any other community member). This is an entirely community-led endeavour and we hope everyone will be encouraged to get involved!
This isn't ready just yet, but look out for an update right here on the blog (or any of our other community areas) once we are fully live and ready!
The PPM repo, while modern and accepted when written, is now mostly viewed as being outdated due to the pervasiveness of deeply nested callbacks. In some instances having nesting six levels deep, and as anyone who's been programming in JavaScript prior to ES6 knows, this can be cumbersome, difficult, and frustrating to work with. That is exactly why the effort has been made to convert the entire codebase into a shallow nested, early return patterned, modern ES6 source utilizing async/await properly. In hopes that a greatly simplified codebase can pave the way for easier modifications in the future.
To show what we are talking about, take this (simplified but real) code as an example:
From this:
this.registerPackage(pack, (error, firstTimePublishing) => {
+ if (error != null) {
+ return callback(error);
+ }
+
+ this.renamePackage(pack, rename, (error) => {
+ if (error != null) {
+ return callback(error);
+ }
+
+ this.versionPackage(version, (error, tag) => {
+ if (error != null) {
+ return callback(error);
+ }
+
+ this.pushVersion(tag, pack, (error) => {
+ if (error != null) {
+ return callback(error);
+ }
+
+ this.waitForTagToBeAvailable(pack, tag, () => {
+ if (originalName != null) {
+ rename = pack.name;
+ pack.name = originalName;
+ }
+ this.publishPackage(pack, tag, { rename }, (error) => {
+ if (firstTimePublishing && error == null) {
+ this.logFirstTimePublishMessage(pack);
+ }
+ return callback(error);
+ });
+ });
+ });
+ });
+ });
+});
+
To this:
const firstTimePublishing = await this.registerPackage(pack);
+await this.renamePackage(pack, rename);
+const tag = await this.versionPackage(version);
+await this.pushVersion(tag, pack);
+
+await this.waitForTagToBeAvailable(pack, tag);
+if (originalName != null) {
+ rename = pack.name;
+ pack.name = originalName;
+}
+
+try {
+ await this.publishPackage(pack, tag, { rename });
+} catch (error) {
+ if (firstTimePublishing) {
+ this.logFirstTimePublishMessage(pack);
+ }
+ throw error;
+}
+
This has been a huge piece of work, which began because they overheard complaints about the PPM codebase being hard to read and understand due to its outdated style. As they had already previously assisted in PPM with the "decaffeination" of the codebase, they were already fairly familiar with it and thought they could help further by "upgrading" the codebase to this modern style.
There has been a lot of focus on code flow during this conversion in order to make the codebase as easy to read and understand as possible for future contributors. Not only was the structure changed, but some code and modules could be removed entirely as their only purpose was to support the previous callback style.
From a Pulsar user's perspective you shouldn't notice anything different at all. This is all about maintenance and modernization of the codebase. We want to make Pulsar as hackable and as easy to contribute as possible and these kinds of efforts go a long way to achieving that goal.
`,9),D={href:"https://github.com/pulsar-edit/ppm/pull/95/",target:"_blank",rel:"noopener noreferrer"},q=a("h2",{id:"macos-binary-signing-issues",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#macos-binary-signing-issues","aria-hidden":"true"},"#"),n(" macOS binary signing issues")],-1),I=a("img",{src:k,height:"200"},null,-1),E={href:"https://github.com/pulsar-edit/pulsar/pull/743",target:"_blank",rel:"noopener noreferrer"},O={href:"https://github.com/DeeDeeG",target:"_blank",rel:"noopener noreferrer"},z={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},W={href:"https://github.com/Meadowsys",target:"_blank",rel:"noopener noreferrer"},F=a("p",null,[n("This issue would only have affected the "),a("code",null,"1.109.0"),n(" releases; the current and upcoming releases will all be signed as normal.")],-1),V=a("h2",{id:"shields-io-badges",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#shields-io-badges","aria-hidden":"true"},"#"),n(" Shields.io badges")],-1),B=a("img",{src:m,height:"100"},null,-1),G={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},R={href:"https://shields.io/",target:"_blank",rel:"noopener noreferrer"},H={href:"https://shields.io/badges/pulsar-downloads",target:"_blank",rel:"noopener noreferrer"},U={href:"https://shields.io/badges/pulsar-stargazers",target:"_blank",rel:"noopener noreferrer"},L={href:"https://web.pulsar-edit.dev/packages/x-terminal-reloaded",target:"_blank",rel:"noopener noreferrer"},J=a("img",{alt:"Pulsar Downloads",src:"https://img.shields.io/pulsar/dt/x-terminal-reloaded"},null,-1),$=a("p",null,"So go grab your badges now to show off your package's stats!",-1),Y=a("h2",{id:"pulsar-on-github-desktop",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#pulsar-on-github-desktop","aria-hidden":"true"},"#"),n(" Pulsar on GitHub Desktop")],-1),K={href:"https://desktop.github.com/",target:"_blank",rel:"noopener noreferrer"},Q=a("img",{src:g,height:"500"},null,-1),X={href:"https://github.com/mdibella-dev",target:"_blank",rel:"noopener noreferrer"},Z={href:"https://github.com/confused-Techie",target:"_blank",rel:"noopener noreferrer"},nn={href:"https://github.com/Daeraxa",target:"_blank",rel:"noopener noreferrer"},an=a("h2",{id:"community-spotlight",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#community-spotlight","aria-hidden":"true"},"#"),n(" Community spotlight")],-1),sn=a("img",{src:i,height:"200"},null,-1),en={href:"https://github.com/twocolours",target:"_blank",rel:"noopener noreferrer"},tn=a("a",{href:"#converting-ppm-s-code-from-callbacks-to-async"},"changes to the PPM codebase",-1),on=a("hr",null,null,-1),pn=a("p",null,"And that wraps things up for this month. We hope you have enjoyed reading about all these changes and updates to Pulsar-related areas. As always, a huge thank you to all of our wonderful community members and a special thanks to all those who donate to the project, which makes this all possible. We hope to see you here again next month!",-1),rn=a("hr",{class:"footnotes-sep"},null,-1),cn={class:"footnotes"},ln={class:"footnotes-list"},un={id:"footnote1",class:"footnote-item"},dn={href:"https://tree-sitter.github.io/tree-sitter/",target:"_blank",rel:"noopener noreferrer"},hn=a("a",{href:"#footnote-ref1",class:"footnote-backref"},"\u21A9\uFE0E",-1);function kn(mn,gn){const s=u("ExternalLinkIcon");return r(),c("div",null,[f,l(" more "),v,a("p",null,[n("One of our main reasons for trying to continue the Atom editor project (rather than moving to another editor or making one from scratch) was to preserve the huge number of community packages that had been created for Atom over the years. Unfortunately, as development slowed on Atom (and especially when the "),a("a",w,[n("sunset"),e(s)]),n(" was announced), some maintainers seem to have either archived their packages or are no longer looking to maintain them.")]),y,a("p",null,[n("If you hadn't already seen or read them, "),a("a",_,[n("@savetheclocktower"),e(s)]),n(" has been writing a series of blog posts that go into detail about all the work that has been going on in Pulsar to support a modern implementation of "),a("a",P,[n("Tree-sitter"),e(s)]),n(". Parts "),a("a",x,[n("one"),e(s)]),n(" and "),a("a",T,[n("two"),e(s)]),n(" are available right now and keep an eye on the Pulsar blog for more updates!")]),S,j,a("p",null,[n("Community member "),a("a",M,[n("@Nemokosch/@twocolours"),e(s)]),n(" \xA0has been spearheading the conversion of our "),a("a",N,[n("PPM codebase"),e(s)]),n(" to upgrade old JavaScript callback style to modern ES6 "),A,n(".")]),C,a("p",null,[n("So a big thank you again to [@Nemokosch] for working on this. You can see the progress in "),a("a",D,[n("this pull request"),e(s)])]),q,I,a("p",null,[n("We discovered an issue recently where it turned out that our macOS binaries weren't being signed by our CI process. After some investigation and "),a("a",E,[n("changes to the CI environment"),e(s)]),n(" by "),a("a",O,[n("@DeeDeeG"),e(s)]),n(", "),a("a",z,[n("@confused-techie"),e(s)]),n(" and "),a("a",W,[n("@Meadowsys"),e(s)]),n(", the binaries are now correctly signed again.")]),F,V,B,a("p",null,[n("Thanks to "),a("a",G,[n("@confused-techie"),e(s)]),n(" and the team at "),a("a",R,[n("Shields.io"),e(s)]),n(", badges for Pulsar packages are now available! This means that if you make a package for Pulsar and publish it to the Pulsar Package Repository, you can now display download and stargazer stats on your README page.")]),a("p",null,[n("Stats can be shown both for "),a("a",H,[n("downloads"),e(s)]),n(" and "),a("a",U,[n("stargazers"),e(s)]),n(". Here is an example of a downloads badge for "),a("a",L,[n("x-terminal-reloaded"),e(s)]),n(":")]),J,$,Y,a("p",null,[n("For those of you who use "),a("a",K,[n("GitHub Desktop"),e(s)]),n(" you will now find (or will soon find) that Pulsar is available as an option for you to select as your editor of choice!")]),Q,a("p",null,[n("Thanks to "),a("a",X,[n("@mdibella-dev"),e(s)]),n(" for adding this for the macOS version, "),a("a",Z,[n("@confused-techie"),e(s)]),n(" for Windows and "),a("a",nn,[n("@Daeraxa"),e(s)]),n(" for the Linux version!")]),an,sn,a("p",null,[n("This month we want to give special attention to "),a("a",en,[n("@Nemokosch/@twocolours"),e(s)]),n(" with their "),tn,n(" already discussed on this update. We love to see these kinds of contributions to Pulsar!")]),on,pn,rn,a("section",cn,[a("ol",ln,[a("li",un,[a("p",null,[n("Image from "),a("a",dn,[n("https://tree-sitter.github.io/tree-sitter/"),e(s)]),n(" - Copyright (c) 2018-2021 Max Brunsfeld "),hn])])])])])}const wn=p(b,[["render",kn],["__file","20231004-Daeraxa-OctoberUpdate.html.vue"]]);export{wn as default}; diff --git a/assets/20231013-savetheclocktower-modern-tree-sitter-part-3.html.479e8a22.js b/assets/20231013-savetheclocktower-modern-tree-sitter-part-3.html.479e8a22.js new file mode 100644 index 0000000000..b08c27cbdb --- /dev/null +++ b/assets/20231013-savetheclocktower-modern-tree-sitter-part-3.html.479e8a22.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-8b6c6124","path":"/blog/20231013-savetheclocktower-modern-tree-sitter-part-3.html","title":"Modern Tree-sitter, part 3: syntax highlighting via queries","lang":"en-US","frontmatter":{"title":"Modern Tree-sitter, part 3: syntax highlighting via queries","author":"savetheclocktower","date":"2023-10-13T00:00:00.000Z","category":["dev"],"tag":["modernization","tree-sitter"]},"excerpt":"Last time I laid out the case
Today I\u2019d like to show you the specific problems that we had to solve in order to pull that off.
\\n","headers":[{"level":2,"title":"The problems","slug":"the-problems","link":"#the-problems","children":[]},{"level":2,"title":"The first challenge: robust querying","slug":"the-first-challenge-robust-querying","link":"#the-first-challenge-robust-querying","children":[{"level":3,"title":"How captures work","slug":"how-captures-work","link":"#how-captures-work","children":[]},{"level":3,"title":"Highlights","slug":"highlights","link":"#highlights","children":[]},{"level":3,"title":"Scope tests","slug":"scope-tests","link":"#scope-tests","children":[]}]},{"level":2,"title":"The second challenge: scoping arbitrary ranges","slug":"the-second-challenge-scoping-arbitrary-ranges","link":"#the-second-challenge-scoping-arbitrary-ranges","children":[{"level":3,"title":"Scope adjustments","slug":"scope-adjustments","link":"#scope-adjustments","children":[]}]},{"level":2,"title":"What does this get us?","slug":"what-does-this-get-us","link":"#what-does-this-get-us","children":[]},{"level":2,"title":"Next time","slug":"next-time","link":"#next-time","children":[]}],"git":{"updatedTime":1697139047000,"contributors":[{"name":"Andrew Dupont","email":"github@andrewdupont.net","commits":1}]},"readingTime":{"minutes":15.79,"words":4738},"filePathRelative":"blog/20231013-savetheclocktower-modern-tree-sitter-part-3.md","localizedDate":"October 13, 2023"}');export{e as data}; diff --git a/assets/20231013-savetheclocktower-modern-tree-sitter-part-3.html.dff0e326.js b/assets/20231013-savetheclocktower-modern-tree-sitter-part-3.html.dff0e326.js new file mode 100644 index 0000000000..7e299e7c97 --- /dev/null +++ b/assets/20231013-savetheclocktower-modern-tree-sitter-part-3.html.dff0e326.js @@ -0,0 +1,100 @@ +import{_ as o}from"./tree-sitter-string-scopes-diagram.2db37c5c.js";import{_ as i,o as r,c as d,a as t,b as e,d as n,e as c,f as a,r as l}from"./app.0e1565ce.js";const p="/assets/tree-sitter-tools-string-tree.3fe45d43.png",u="/assets/tree-sitter-comment-scopes-diagram.88665730.png",h="/assets/tree-sitter-tools-comment-tree.f78df2f6.png",m="/assets/tree-sitter-snippet-context-example.f7c0921c.webm",g="/assets/tree-sitter-snippet-context-example.8124fdcd.mp4",v={},f={href:"https://pulsar-edit.dev/blog/20230927-savetheclocktower-modern-tree-sitter-part-2.html",target:"_blank",rel:"noopener noreferrer"},b=t("strong",null,[e("make it so that a Tree-sitter grammar can do "),t("em",null,"anything"),e(" a TextMate grammar can do.")],-1),w=t("p",null,"Today I\u2019d like to show you the specific problems that we had to solve in order to pull that off.",-1),y={href:"https://web.pulsar-edit.dev/packages/tree-sitter-tools",target:"_blank",rel:"noopener noreferrer"},k=t("code",null,"tree-sitter-tools",-1),_={href:"https://tree-sitter.github.io/tree-sitter/playground",target:"_blank",rel:"noopener noreferrer"},j=a('The legacy Tree-sitter integration into Atom used its own system for mapping tree nodes to scope names, but it had two major limitations that prevented it from matching the scope names produced by a TextMate grammar:
The need to write an Atom-specific bridge between node names and scope names served as evidence that Tree-sitter need its own system for more easily working with syntax trees \u2014 one that would prevent every Tree-sitter consumer from having to reinvent the wheel.
',4),x={href:"https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries",target:"_blank",rel:"noopener noreferrer"},q={href:"https://en.wikipedia.org/wiki/Scheme_(programming_language)",target:"_blank",rel:"noopener noreferrer"},T={href:"https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector",target:"_blank",rel:"noopener noreferrer"},S=t("code",null,"document.querySelector",-1),I=a('Right now, though, let\u2019s just focus on syntax highlighting. And remember those two limitations that I described above, because we\u2019ll have to solve them both before this article is done.
I\u2019ll remind you of our example from part 2: the scope names applied to a double-quoted string in JavaScript.
Our first goal is to make it so that our JavaScript Tree-sitter grammar can apply these same scopes to the same buffer ranges. But Tree-sitter works very differently to a TextMate grammar, so it\u2019s not immediately obvious how we can pull this off. Let\u2019s reason through it.
Last time I showed you this excerpt from tree-sitter-javascript
\u2019s highlights.scm
file:
[
+ (string)
+ (template_string)
+] @string
+
Once you\u2019re familiar with query syntax, the outcome of this query is clear: all JavaScript strings will be captured and given a name of @string
. Somewhere within the tree-sitter highlight
code path, those capture names are mapped to various colors, and are applied to the captured buffer ranges. Anything that gets captured as @string
will have one color in the output; anything that gets captured as @keyword
will have a different color; and so on.
Let\u2019s imagine that Pulsar has a similar system. In order to keep this article from putting you to sleep, I won\u2019t get into the details of exactly how we do it, but the machinery is in place. Instead of a capture name like @string
, we\u2019ll be choosing more verbose names like @string.quoted.double.js
, but the principles are the same.
We also won\u2019t be talking about how Pulsar knows which areas of the buffer to re-highlight as the user makes changes, nor how Pulsar combines the results of multiple parsers (for example, JavaScript embedded in HTML). These topics may be visited in future articles, but today we\u2019re just talking about how to use Tree-sitter queries to identify arbitrary ranges and give them the scope names that we want.
What does a string look like in a Tree-sitter tree? Let\u2019s create a new document with nothing but the following contents:
"like this";
+
In Tree-sitter parsers, nodes that matter to semantics (like string_content
) tend to have names, whereas other nodes (like the delimiters) are \u201Canonymous\u201D nodes. Anonymous nodes can still be queried against like named nodes, so it feels like it\u2019ll be pretty easy to apply the scopes that we want.
So let\u2019s open our grammar\u2019s highlights.scm
file and give it a try. (In Pulsar\u2019s \u201Cdev mode\u201D \u2014 which you can trigger with the --dev
flag on the command line \u2014 grammar authors can even make changes to their query files and see them take effect instantly when they save!)
(string) @string.quoted.double.js
+
That\u2019s not bad, but it\u2019s too specific. In tree-sitter-javascript
, the string
node applies for both single-quoted and double-quoted strings, with the difference being reflected only in the anonymous nodes. (Template strings, as we saw above, have their own node type.)
How do we distinguish single-quoted strings from double-quoted strings? Here\u2019s one thing we could do:
((string) @string.quoted.single.js
+ (#match? @string.quoted.single.js "^'"))
+((string) @string.quoted.double.js
+ (#match? @string.quoted.single.js "^\\""))
+
The built-in #match?
predicate allows us to reject possible matches when their contents don\u2019t match a given regular expression. Here we\u2019re telling the query engine to distinguish between string
nodes whose text starts with '
and those whose text starts with "
.
We\u2019ll be using the #match?
predicate a lot. Unlike some of the other predicates we\u2019ll see shortly, it\u2019s implemented by the web-tree-sitter
bindings, so Tree-sitter on its own is able to reject would-be captures that don\u2019t pass it. By the time we see the list of captures, those that have failed a #match?
test have already been filtered out.
In this case, though, there\u2019s an even easier way to tell these strings apart:
(string "'") @string.quoted.single.js
+(string "\\"") @string.quoted.double.js
+
As I mentioned, we can query for the presence of anonymous nodes. So the first line will match any string
that contains at least one anonymous node child named '
, and the second line will match any string
that contains at least one anonymous node child named "
.
Since the capture name is on the outside of the closing parenthesis, the capture name applies to the whole string
.
We don\u2019t have to be more specific; if that anonymous node exists at all, then it\u2019s used as a delimiter on both sides of the string. And this query won\u2019t match any potential false positives \u2014 like a double-quoted string that happens to have a '
somewhere inside of it \u2014 because the parser is too smart to get tripped up by that sort of thing.
This is one reason why it\u2019s very easy to make a new Tree-sitter grammar if someone has done the work of writing a Tree-sitter parser for the given language. If we were writing a TextMate grammar, we\u2019d have to care about a lot more of these edge cases, but a Tree-sitter parser will have handled them for us already.
We can already tell that the expressiveness of Tree-sitter\u2019s query system will go a long way toward solving the first of the two problems we described above. Last time around, Atom developers had to invent a system for querying the tree, but we get a much more powerful system for free.
Tools like anonymous nodes and #match?
predicates can get us quite far on their own, but they can\u2019t solve all of our problems. We still have to scope the quotation marks themselves, and we may think we know how to do it:
(string "'" @punctuation.definition.string.begin.js)
+
By putting the capture name immediately after the "'"
, we can target that anonymous node and give it a name. But remember that there are two delimiters! We want to give one scope name to the beginning delimiter and a different scope name to the ending delimiter. As we\u2019ve written it, this rule would match both delimiters.
Luckily, Tree-sitter gives us the tools to write our own predicates. Instead of trying to make it aware of our application-specific concerns, we can use the generic predicates #is?
and #is-not?
to mark certain query captures with data, then use that data to filter the results however we like.
The downside is that Tree-sitter can\u2019t approve or reject captures with these predicates on its own like it can with #match?
. Instead, we\u2019ll have to \u201Cprocess\u201D these captures after the fact and filter them manually. But it\u2019s worth the effort because\xA0it lets us use whatever logic we want \u2014\xA0even Pulsar-specific logic that means nothing to Tree-sitter.
Let\u2019s illustrate.
(string
+ "'" @punctuation.definition.string.begin.js
+ (#is? test.first "true"))
+
The two parameters after #is?
are arbitrary values of my own invention. Tree-sitter simply treats them as a key and value and applies some metadata to this capture. Any nodes that get captured by this query will contain some data under assertedProperties
:
capture.assertedProperties["test.first"]; //-> "true"
+
In fact, I can omit that second argument; for boolean tests like this one, the presence of the property is all we need:
(string
+ "'" @punctuation.definition.string.begin.js
+ (#is? test.first))
+(string
+ "'" @punctuation.definition.string.end.js
+ (#is? test.last))
+
Using (#is? test.first)
is like having Tree-sitter attach a Post-It note to a capture object with the text \u201Cremember to assert test.first
later\u201D written on it. Tree-sitter doesn\u2019t know or care what that means, but it assumes we will.
And in this case, test.first
corresponds to a function we\u2019ve written that looks like this:
function first(node) {
+ if (!node.parent) return true;
+ return node?.parent?.firstChild?.id === node.id;
+}
+
This function first ensures that root nodes (which have no parent) will always pass the test. Then it compares our node to its parent\u2019s first child. If they\u2019re equal, the test passes. If they\u2019re not, then the captured node isn\u2019t the first child of its parent, and we can ignore it.
The logic for our last
function is identical, except that it compares our node
to node.parent.lastChild
.
Even better: the existence of #is-not?
means that we get negation practically for free. Suppose I did this:
(string
+ "'" @punctuation.definition.string.end.js
+ (#is-not? test.first))
+
Then the metadata would exist in a different place\u2026
"test.first" in capture.refutedProperties; //-> true
+
\u2026and I\u2019d know to ignore this capture unless my first
function fails for this node.
So now we\u2019ve got a way to filter capture names by any criteria we can think of. If we can test for it in JavaScript, it can be used as a predicate in Tree-sitter queries.
We call these custom predicates scope tests, and we\u2019ve grouped them under a test.
namespace for reasons that may make more sense later. Scope tests are a crucial tool for solving the first of those two problems we described earlier: they let us query for tree nodes in arbitrary ways that the legacy system simply couldn\u2019t.
And because scope tests are just JavaScript, we\u2019re able to use some oddball criteria for accepting or rejecting captures. Consider these examples:
((program) @source.js.embedded
+ (#is? test.injection))
+
+(variable_declarator
+ name: (identifier) @constant.other.foo
+ (#match? @constant.other.foo "^[_A-Z]+$")
+ (#is? test.config "language-foo.highlightAllCapsIdentifiersAsConstants"))
+
Both of these are scope tests that grammar query files can use. The first one applies a scope only if we\u2019re in an injection layer \u2014 for instance, if this is JavaScript inside of a SCRIPT
tag in an HTML file. The second one applies a scope only if the user has enabled a certain configuration option. Neither one has anything to do with Tree-sitter itself, but we can use them in Tree-sitter query files all the same.
There are other scope tests that we\u2019ve found to be quite useful \u2014\xA0tests which any grammar author can use in their own query files:
#set!
predicateBut for our current goal \u2014 applying punctuation
scopes to string delimiters \u2014 implementing test.first
and test.last
is enough to get us the outcome we want.
You may remember from earlier that the legacy Tree-sitter integration used a CSS-like syntax to describe nodes. It supported combinators like >
and even pseudoclasses like :nth-child
, but not much else. Tree-sitter\u2019s own query system can do much more than that \u2014 and it\u2019s extensible, so we can add our own logic wherever we need it.
So we\u2019ve done it! We now have the ability to scope a JavaScript string identically between our two different grammar systems. And we\u2019ve moved past the legacy system\u2019s first drawback: a brittle query system. Our first challenge has been vanquished.
To solve the second drawback \u2014\xA0inability to scope the correct ranges \u2014 let\u2019s create another challenge.
TextMate grammars will scope comments differently based on whether they\u2019re line comments or block comments. There\u2019s also a convention to annotate comment.line
scopes with their delimiter type:
// this is a comment
+
A typical TextMate grammar for JavaScript would scope this comment as comment.line.double-slash.js
, and would further scope the //
as punctuation.definition.comment.js
.
Can we do that in a Tree-sitter grammar with the tools we\u2019ve already got? Let\u2019s inspect what our example comment looks like in a Tree-sitter tree:
Hmm. No anonymous nodes or anything. Just one node called comment
.
It\u2019s a strong Tree-sitter convention that various sorts of code comments are all represented by nodes called comment
. So this query would do the right thing in our example\u2026
(comment) @comment.line.double-slash.js
+
\u2026but would be incorrect in other scenarios because it would match block comments as well as line comments.
But we can use #match?
to distinguish between the two kinds of comments:
((comment) @comment.line.double-slash.js
+ (#match? @comment.line.double-slash.js "^\\/\\/"))
+
+((comment) @comment.block.js
+ (#match? @comment.block.js "^\\/*"))
+
In the latter case, we don\u2019t have to test for the presence of */
at the end. If the contents of the comment begin with /*
, that\u2019s all the information we need; we know it must end with */
, or else the parser wouldn\u2019t have classified it as a comment.
But what about our punctuation.definition.comment.js
scope? Sadly, tree-sitter-javascript
(and most other parsers) don\u2019t make it easy to target the comment delimiters themselves. Comment delimiters, unlike string delimiters, usually aren\u2019t available as anonymous nodes.
Hence the legacy Tree-sitter system has never been able to annotate that //
with the punctuation
scope it needed. We\u2019ll need to solve this ourselves.
In this case, it\u2019d be more convenient if there were a node for the //
we want to scope. But we control the internals of the editor, and we can tell it to apply scope names to whatever buffer ranges we want. When the tree doesn\u2019t do all of our work for us, it just means we have to try a bit harder.
In this case, the comment
node tells us where the comment starts in the buffer. And once it passes the #match?
predicate, we know that it starts with //
, so it must end two characters later. Not the hardest problem to solve!
The trickiest part will be figuring out how to describe these buffer positions in a query.
What if we did this?
((comment) @comment.line.double-slash.js
+ (#match? @comment.line.double-slash.js "^//"))
+
+((comment) @punctuation.definition.comment.js
+ (#match? @punctuation.definition.comment.js "^//")
+ (#set! adjust.endAfterFirstMatchOf "^//"))
+
Here we\u2019re capturing the same thing twice under different names. In the second case, we\u2019re using #set!
\u2014\xA0a predicate very similar to #is?
and #is-not?
\u2014 to attach a qualifier: instead of stopping at the end of the line comment, stop\xA0right after the //
near the beginning.
Just like #is?
predicates are represented on captures under assertedProperties
and #is-not?
predicates are represented under refutedProperties
, #set!
predicates are represented in their own bucket simply called properties
.
Note the semantic difference between a predicate that ends in !
and one that ends in ?
. In this case we\u2019re not setting up a test for the capture to pass or fail; we\u2019re attaching a side effect to the capture. Imagine a Post-It note attached to the capture that says \u201Cremember to adjust the range for this capture to end at X.\u201D
I mentioned earlier that all nodes remember their corresponding buffer range \u2014 starting at row W and column X, ending at row Y and column Z. By default, this is the range against which a scope is applied. But in this case, the adjust.endAfterFirstMatchOf
predicate reminds us to execute a regular expression match on the node\u2019s contents and move the ending position to the end of that match, instead of the node\u2019s natural ending point.
And how would we handle the delimiters of a block comment?
((comment) @comment.block.js
+ (#match? @comment.block.js "^/\\\\*"))
+
+((comment) @punctuation.definition.comment.begin.js
+ (#match? @punctuation.definition.comment.begin.js "^/\\\\*")
+ (#set! adjust.endAfterFirstMatchOf "^/\\\\*"))
+
+((comment) @punctuation.definition.comment.end.js
+ (#match? @punctuation.definition.comment.end.js "\\\\*/$")
+ (#set! adjust.startBeforeFirstMatchOf "\\\\*/$"))
+
+
We can scope the opening delimiter the same way. To scope the ending delimiter, we move the head of the range to the position at the beginning of a regex match. (And for situations where you want to move both the head and the tail at once, there\u2019s adjust.startAndEndAroundFirstMatchOf
).
Let\u2019s look at another example.
function SomeComponent(props) {
+ return <SomeOtherComponent {...props} />;
+}
+
Let\u2019s say we want to scope the />
at the end of the self-closing tag. Tree-sitter represents that as two separate anonymous nodes \u2014 /
and >
.
So we\u2019ve got a different problem here: there\u2019s no single node that includes both boundaries of the range we want to scope. How can we make the scope span two adjacent nodes?
We could probably use a pattern-based solution here like we did above. But we could also leverage a useful feature of how nodes are represented in the tree.
`,9),ee={href:"https://developer.mozilla.org/en-US/docs/Web/API/Node",target:"_blank",rel:"noopener noreferrer"},te=t("code",null,"parentNode",-1),se=t("code",null,"nextSibling",-1),ne=a(`A similar system exists for Tree-sitter nodes, and it gives us a simple way to describe relationships between nodes:
((jsx_self_closing_element
+ ; The "/>" in \`<Foo />\`, extended to cover both anonymous nodes at once.
+ "/") @punctuation.definition.tag.end.js
+ (#set! adjust.startAt lastChild.previousSibling.startPosition)
+ (#set! adjust.endAt lastChild.endPosition))
+
Here we\u2019re capturing the entire self-closing element, then moving the ends of the range to the specific boundaries that we need: starting at the beginning of second-to-last node child, and ending at the end of the last node child. The /
and >
will always be the last two children of the node we captured, so this is a simple and repeatable way to describe the boundaries we want.
So in order to pass our \u201CJavaScript line comment\u201D challenge, we\u2019ve had to invent another feature called scope adjustments. With the infrastructure we\u2019ve already got, scope adjustments are an easy thing to add: a scope adjustment is just a function that accepts a capture object and returns a range. We process them in the post-capture phase immediately before we apply scope tests.
Scope adjustments are our answer to the legacy system\u2019s second limitation. They allow us to embrace the syntax tree when it works in our favor, but still break free of it whenever we need to.
Some of you, having gotten this far, might wonder to yourselves: if it takes this much effort just to get feature parity with a TextMate grammar, why not just stick with TextMate grammars?
One answer is simple: despite how complex this may seem, and how many implementation details I\u2019ve hidden from you, the effort it\u2019s taken to integrate Tree-sitter grammars into Pulsar is much, much less than the effort it originally took to integrate TextMate grammars into Atom.
But the message here can\u2019t just be \u201Cwe implemented a cool new thing that you won\u2019t notice at all!\u201D The whole point is that this is better than \u2014 not just equivalent to \u2014\xA0what we had before, and not just for the few of us who write grammars.
So let me give you an example of\xA0something that was very hard with a TextMate grammar, but is now quite easy.
When a scope name might be useful for semantic reasons, but not for syntax highlighting reasons, TextMate grammars add scope names that use the meta
namespace. You might see some meta
scope names when you run the \u201CLog Cursor Scope\u201D command, but most of them don\u2019t have an effect on how your code looks. Yet that information is visible to snippets, settings, and commands, and it can be very useful.
In the language-javascript
package, Pulsar defines a fun
snippet that expands as follows, with the string functionName
highlighted:
function functionName() {}
+
That\u2019s great, but there\u2019s more than one syntax for defining functions. Imagine if you could use fun
inside of a class body and have it expand to the correct syntax for defining an instance method:
class Foo {
+ functionName() {}
+}
+
Or inside of an object literal \u2014 much like the class body syntax, but with a trailing comma so as to prevent syntax errors:
const Foo = {
+ functionName() {},
+};
+
TextMate grammars weren\u2019t built with this sort of thing in mind. With much effort, you could probably pull it off, but only by making the grammar so complex that it might as well be a parser. But Tree-sitter is already a parser. It understands your code well enough to make these things easy.
With our rich syntax tree, we can now apply meta
scope names rather liberally, marking sections of the buffer with useful metadata that can be used by commands and snippets. We can scope the inside of a class body and the inside of an object literal:
; The interior of a class body.
+((class_body) @meta.block.class.js
+ ; Start after \`{\`\u2026
+ (#set! adjust.startAt firstChild.endPosition)
+ ; \u2026and end before \`}\`.
+ (#set! adjust.endAt lastChild.startPosition))
+
+; The inside of an object literal.
+((object) @meta.object.js
+ ; Start after \`{\`\u2026
+ (#set! adjust.startAt firstChild.endPosition)
+ ; \u2026and end before \`}\`.
+ (#set! adjust.endAt lastChild.startPosition))
+
By defining these two scope names, we\u2019ve exposed these concepts to all the systems that consume scope names, including snippets.
The language-javascript
package already defines fun
one way, but we can redefine it for more specific scopes:
'.source.js .meta.block.class.js':
+ 'Function':
+ 'prefix': 'fun'
+ 'body': '\${1:functionName}($2) {\\n\\t$0\\n}'
+
+'.source.js .meta.object.js':
+ 'Function':
+ 'prefix': 'fun'
+ 'body': '\${1:functionName}($2) {\\n\\t$0\\n},'
+
Now we\u2019ve made the fun
snippet much more useful. When we type fun
and press Tab, the snippets
package will pick the version that matches the context of the cursor most closely.
This will work identically whether you use tab triggers or choose your snippets from an autocomplete menu.
This change isn\u2019t just theoretical; it\u2019s been implemented in the language-javascript
grammar package, and it shipped with Pulsar 1.109.
Syntax highlighting isn\u2019t the only way that Tree-sitter\u2019s query system can make our lives easier. In the next installment we\u2019ll tackle two tasks that the legacy Tree-sitter integration never addressed: indentation and code folding.
',26);function de(ce,le){const s=l("ExternalLinkIcon");return r(),d("div",null,[t("p",null,[t("a",f,[e("Last time I laid out the case"),n(s)]),e(" for why we chose to embrace TextMate-style scope names, even in newer Tree-sitter grammars. I set a difficult challenge for Pulsar: "),b]),w,c(" more "),t("p",null,[e("If you\u2019d like to follow along with these code examples in your own Pulsar installation, I\u2019d suggest installing the "),t("a",y,[k,e(" package"),n(s)]),e(". It includes a language grammar for the Tree-sitter query files we\u2019ll be spending most of this article writing, and it lets us visualize the syntax trees that Tree-sitter produces.")]),t("p",null,[e("If you just want to get a sense of what a Tree-sitter tree looks like, you can use the "),t("a",_,[e("Playground on the Tree-sitter web site"),n(s)]),e(".")]),j,t("p",null,[e("So Tree-sitter added a powerful "),t("a",x,[e("query system"),n(s)]),e(", with a Lisp-like syntax most directly influenced by "),t("a",q,[e("Scheme"),n(s)]),e(". This system wasn\u2019t around for the legacy implementation to use \u2014 but it\u2019s here for us now, and it\u2019s going to make our job much easier.")]),t("p",null,[e("Tree-sitter query syntax does the same thing for a Tree-sitter syntax tree that CSS selector syntax does for HTML: it gives us a terse way to describe a set of nodes in a tree. And just as people eventually realized that CSS selector syntax was useful for more than just styling \u2014 see "),t("a",T,[S,n(s)]),e(" \u2014 you\u2019ll soon see that Tree-sitter queries are useful for more than just making strings green.")]),I,t("p",null,[e("Tree-sitter itself "),t("a",A,[e("has demonstrated"),n(s)]),e(" how query files can be used to highlight code. If a parser has a "),W,e(" file defined in its repository, the CLI will allow you to run "),P,e(" on arbitrary input. It\u2019ll parse the input, figure out which parser should do the job, use that parser\u2019s "),M,e(" to map certain nodes to query capture names, and then emit highlighted output in your terminal.")]),L,t("p",null,[e("Using the "),t("a",C,[e("tree-sitter-tools package"),n(s)]),e(", I can open an inspector pane and look at the raw tree for this string:")]),B,N,t("p",null,[e("Most parsers build "),t("a",H,[J,e(" syntax trees"),n(s)]),e(". In the process of making the tree, they discard lots of information that isn\u2019t important to the parser\u2019s goal. But Tree-sitter builds "),O,e(" syntax trees! Every character in our JavaScript file will end up being represented by at least one node, and every node remembers the exact buffer range it covers. If it didn\u2019t, we wouldn\u2019t be able to use it for syntax highlighting.")]),F,t("p",null,[e("Surprisingly, we can\u2019t solve this problem without some external help. Tree-sitter queries "),t("a",$,[e("have a concept of \u201Canchors\u201D"),n(s)]),e(" that can enforce positioning of children \u2014 for instance, targeting only the first node child of a parent \u2014 but they can be used only for "),E,e(" nodes, not anonymous nodes. "),Y]),R,t("p",null,[e("The logic for applying tests and winnowing a list of raw captures exists in Pulsar in a class called "),U,e(". That "),V,e(" function defined above is present at "),t("a",z,[D,n(s)]),e(", and there\u2019s logic in "),X,e(" that matches up a "),Z,e(" property to that function.")]),G,t("p",null,[e("So in the absence of any node boundary at the position we want, we\u2019ve found a rather simple alternative way to express that position. We still have to write "),t("a",K,[e("our own code"),n(s)]),e(" to make it happen, but that won\u2019t be a problem. Since we already have to loop through our captures to apply scope tests, we might as well use that opportunity to tweak the range of a capture if we need to.")]),Q,t("p",null,[e("You might be familiar with how "),t("a",ee,[e("DOM nodes in the browser"),n(s)]),e(" are traversible via properties like "),te,e(", "),se,e(", and so on. If you\u2019ve got a reference to a particular DOM node, you can use those properties to jump from that node to any other node in the tree, as long as you you know how the two nodes are related.")]),ne,t("p",null,[e("This syntax for describing a position relative to a given node is something we call a "),ae,e(". If you\u2019ve ever used a function like "),t("a",oe,[e("Lodash\u2019s "),ie,n(s)]),e(", you might feel at home with this syntax \u2014 it resolves a chain of property lookups all at once, and fails gracefully if any of them aren\u2019t present. It\u2019ll come in handy later for other purposes.")]),re])}const he=i(v,[["render",de],["__file","20231013-savetheclocktower-modern-tree-sitter-part-3.html.vue"]]);export{he as default}; diff --git a/assets/20231016-Daeraxa-v1.110.0.html.1a72c098.js b/assets/20231016-Daeraxa-v1.110.0.html.1a72c098.js new file mode 100644 index 0000000000..c279fab9dd --- /dev/null +++ b/assets/20231016-Daeraxa-v1.110.0.html.1a72c098.js @@ -0,0 +1 @@ +const a=JSON.parse(`{"key":"v-e8324d12","path":"/blog/20231016-Daeraxa-v1.110.0.html","title":"Armed with a big ol' can of Raid: Pulsar 1.110.0 is available now!","lang":"en-US","frontmatter":{"title":"Armed with a big ol' can of Raid: Pulsar 1.110.0 is available now!","author":"Daeraxa","date":"2023-10-16T00:00:00.000Z","category":["dev"],"tag":["release"]},"excerpt":"Armed with a big ol' can of Raid: Pulsar 1.110.0
And that just about covers things for this release. We hope you have enjoyed reading this and continue to enjoy using Pulsar. As ever, a huge thank you to our generous donors and community, without whom this project would not be possible.
Until next time, happy coding, and see you amongst the stars!
editor.preferredLineLength
configurable within wrap-guide
when changing wrap-guide.columns
language-php
that would lose the $
characterautocomplete-html
when handling EJS filesnode-gyp
, allowing newer versions of C/C++ compiler toolchains and Python to be used (also dropped support for Python 2.x!)Last time we looked at Tree-sitter\u2019s query system and showed how it can be used
Today we\u2019ll look at two other systems \u2014 indentation hinting and code folding \u2014 and I\u2019ll explain how queries can be used to support each one.
\\n","headers":[{"level":2,"title":"Indentation","slug":"indentation","link":"#indentation","children":[{"level":3,"title":"How TextMate grammars do it","slug":"how-textmate-grammars-do-it","link":"#how-textmate-grammars-do-it","children":[]},{"level":3,"title":"How legacy Tree-sitter grammars do it","slug":"how-legacy-tree-sitter-grammars-do-it","link":"#how-legacy-tree-sitter-grammars-do-it","children":[]},{"level":3,"title":"Working within the system","slug":"working-within-the-system","link":"#working-within-the-system","children":[]},{"level":3,"title":"Getting creative","slug":"getting-creative","link":"#getting-creative","children":[]}]},{"level":2,"title":"Code folding","slug":"code-folding","link":"#code-folding","children":[{"level":3,"title":"How TextMate grammars do it","slug":"how-textmate-grammars-do-it-1","link":"#how-textmate-grammars-do-it-1","children":[]},{"level":3,"title":"How legacy Tree-sitter grammars do it","slug":"how-legacy-tree-sitter-grammars-do-it-1","link":"#how-legacy-tree-sitter-grammars-do-it-1","children":[]},{"level":3,"title":"Using queries to define fold ranges","slug":"using-queries-to-define-fold-ranges","link":"#using-queries-to-define-fold-ranges","children":[]}]},{"level":2,"title":"Why is this so complicated?","slug":"why-is-this-so-complicated","link":"#why-is-this-so-complicated","children":[]},{"level":2,"title":"Next time","slug":"next-time","link":"#next-time","children":[]}],"git":{"updatedTime":1698809922000,"contributors":[{"name":"Andrew Dupont","email":"github@andrewdupont.net","commits":4}]},"readingTime":{"minutes":13.28,"words":3983},"filePathRelative":"blog/20231031-savetheclocktower-modern-tree-sitter-part-4.md","localizedDate":"October 31, 2023"}');export{e as data}; diff --git a/assets/20231031-savetheclocktower-modern-tree-sitter-part-4.html.c1f7638e.js b/assets/20231031-savetheclocktower-modern-tree-sitter-part-4.html.c1f7638e.js new file mode 100644 index 0000000000..a4603ad412 --- /dev/null +++ b/assets/20231031-savetheclocktower-modern-tree-sitter-part-4.html.c1f7638e.js @@ -0,0 +1,86 @@ +import{_ as l,o as c,c as h,a as t,b as e,d as n,w as u,e as p,f as a,r as o}from"./app.0e1565ce.js";const i="/assets/tree-sitter-simple-indentation.6a2dba5c.webm",r="/assets/tree-sitter-simple-indentation.7d0eae68.mp4",m="/assets/tree-sitter-tools-indentation-example.52c421af.png",v="/assets/tree-sitter-advanced-indentation-part-1.98936604.webm",b="/assets/tree-sitter-advanced-indentation-part-1.33a3a459.mp4",g="/assets/tree-sitter-advanced-indentation-part-2.e941a3ba.webm",f="/assets/tree-sitter-advanced-indentation-part-2.9c4500ba.mp4",w="/assets/tree-sitter-simple-fold-example.8070404a.png",y="/assets/tree-sitter-tools-css-block.47b94e88.png",k="/assets/tree-sitter-javascript-block-comment.812590a9.png",x="/assets/tree-sitter-block-comment-code-fold.b6a82fbf.webm",_="/assets/tree-sitter-block-comment-code-fold.28ab19f5.mp4",T="/assets/tree-sitter-simple-folding.5bf37994.webm",q="/assets/tree-sitter-simple-folding.ac70c9fb.mp4",I={},S=t("p",null,"Today we\u2019ll look at two other systems \u2014 indentation hinting and code folding \u2014 and I\u2019ll explain how queries can be used to support each one.",-1),j=a(`Our programmer predecessors might scoff at how much our editors coddle us, but I\u2019ll say it anyway: I hate it when my editor doesn\u2019t know when to indent. In particular, C-style languages like JavaScript have predictable and simple rules about indentation, so when an editor guesses wrong, it\u2019s like a burst of static in the middle of a song.
If I type
if {
+
and press Return, all major code editors will indent the next line for me by one level. There\u2019s no reason for an editor to force me to press Tab myself \u2014 it\u2019s obvious from context.
And once I\u2019m done with the conditional and type }
on its own line, my editor should recognize that this line isn\u2019t part of the indented block, and should decrease the indentation by one level automatically.
How does Pulsar do this now? And how can we swap in our own system for doing it with Tree-sitter?
Pulsar, like Atom before it, uses an indentation hinting system based on the system from TextMate grammars. It works a bit like this:
increaseIndentPattern
. If there\u2019s a match, it concludes that the next line should start with an extra level of indentation.}
or end
\u2014\xA0signify the end of a block, a TextMate grammar will test the current line against a regular expression called decreaseIndentPattern
over and over as the user types. If that pattern ever matches, the current line will be dedented one level.For JavaScript, imagine that increaseIndentPattern
can be described as \u201Can opening brace without a matching closing brace,\u201D and decreaseIndentPattern
can be described as \u201Ca closing brace that is not preceded by an opening brace.\u201D Each pattern would probably need to match more than just those situations, but that\u2019s the most important pattern to recognize by far.
Let\u2019s look at that screencast again to see these rules in action:
The first rule comes into play when we press Return at the end of line 2. A newline is inserted, and the cursor is correctly indented by one additional level.
The second rule reacts after we type a }
on line 4: now the line\u2019s content matches decreaseIndentPattern
, and the line is dedented automatically.
The same way. No, seriously.
It was obviously meant to be temporary, but legacy Tree-sitter grammars rely on the same increaseIndentPattern
and decreaseIndentPattern
that TextMate grammars use. We should come up with a Tree-sitter\u2013based system instead.
TextMate\u2019s deep embrace of regular expressions is a double-edged sword: it makes simple things easy, but it makes complex things nearly impossible. Instead of testing lines against a single regular expression \u2014 which can quickly get unwieldy as it expands to handle all possible indentation hinting scenarios \u2014 we can use Tree-sitter\u2019s query system to identify code features that would affect indentation.
',20),P={href:"https://github.com/mauricioszabo",target:"_blank",rel:"noopener noreferrer"},H={href:"https://github.com/nvim-treesitter/nvim-treesitter/blob/v0.9.1/doc/nvim-treesitter.txt#L210",target:"_blank",rel:"noopener noreferrer"},M=a(`We could\u2019ve just adopted Neovim\u2019s system wholesale. But I wanted a system that kept the same decision points as the TextMate system for conceptual simplicity. So we\u2019ve built a system that will abide by the contract described above\u2026
\u2026except using its own logic:
increaseIndentPattern
, we\u2019ll instead run a query capture against the previous line and look at the results to figure out whether to indent the new line.decreaseIndentPattern
, we\u2019ll run a query capture against that line with each keystroke and look for results that imply we should dedent that line relative to the line above.Remember that Tree-sitter produces concrete syntax trees; every node represents an actual buffer range. The things that are typically stripped from abstract syntax trees, like punctuation, are still present in a Tree-sitter tree, and can be queried. That\u2019s good news for us: looking at punctuation is a great way to predict how lines should be indented.
So let\u2019s start here. Imagine this content exists in a file called indents.scm
:
"{" @indent
+"}" @dedent
+
This is maybe the simplest possible system for describing indentation in a C-style language. For some languages, like CSS, this gets us 99% of the way to a perfect indentation system. But how does it work?
`,9),A={href:"https://web.pulsar-edit.dev/packages/tree-sitter-tools",target:"_blank",rel:"noopener noreferrer"},L=t("code",null,"tree-sitter-tools",-1),C={href:"https://pulsar-edit.dev/blog/20230925-savetheclocktower-modern-tree-sitter-part-1.html#can-i-use-this-new-implementation-now",target:"_blank",rel:"noopener noreferrer"},B=t("li",null,"Open a new document, change the grammar to CSS, and type some sample CSS.",-1),R=t("li",null,[e("Run the "),t("strong",null,"Tree Sitter Tools: Open Inspector For Editor"),e(" command.")],-1),W=t("li",null,[e("Toggle the \u201CAnonymous nodes\u201D option to "),t("strong",null,"Show"),e(".")],-1),E=t("li",null,[e("Paste that code block into the appropriate text box and click on "),t("kbd",null,"Run Query"),e(".")],-1),N=a('You can see the query matches illustrated in the text editor in real time, and you can match up the colors to the @indent
and @dedent
captures. You can even type new content (as in the examples below) and see the captures update in real time!
Let\u2019s say the user is writing a CSS file, and the cursor is represented by the |
character:
body {|
+
If the user were to press Return, we\u2019d run a query capture on a specific range of the buffer: from the start of row 1 to wherever the cursor was. The opening brace on row 1 would produce a single capture called @indent
. Based on that information, we\u2019d know that row 2 should be indented by one level.
But what if the file looked like this instead?
body { font-family: 'Helvetica', sans-serif; }|
+
If the user were to press Return, we\u2019d run the same query capture, and it would match twice: one @indent
capture and one @dedent
capture. Those captures would cancel each other out. We\u2019d know that the opening brace we saw had already been balanced by a later closing brace, and we\u2019d know that row 2 should not increase its indentation level.
Now let\u2019s look at one more example:
body {
+ font-family: 'Helvetica', sans-serif;
+ |
+
After typing one property-value pair and pressing Return, we\u2019re on row 3. Should this line be dedented? It depends on what we\u2019re about to type! If we\u2019re about to type }
, then the answer is yes \u2014 but if we\u2019re typing anything else, like another property-value pair, then the answer is no. That\u2019s why Pulsar won\u2019t decide whether to dedent until we start typing. If we were to type }
, our Tree-sitter grammar would run a query capture on the current line, spot the @dedent
match, and respond by dedenting the current line one level from the line above.
For more complex C-style languages like JavaScript, here\u2019s a better starting point for indents.scm
:
; Any of these characters should trigger indent\u2026
+[ "{" "(" "[" ] @indent
+
+; \u2026and any of these should trigger dedent.
+[ "}" ")" "]" ] @dedent
+
There are major simplicity benefits to targeting these anonymous nodes instead of more abstract nodes. Most folks\u2019 indentation styles tend to align with delimiter usage, so the more tightly we can bind to the delimiters themselves, the more accurate the hinting will be. And anonymous nodes are safer to rely upon as the user types and the syntax tree is in flux. Sometimes we have to \u201Cfilter out\u201D false positives \u2014 for instance, curly braces in JavaScript that signify template string interpolations instead of statement blocks \u2014 but that\u2019s a small price to pay.
I\u2019m hiding some of the complexity from you, but less than you\u2019d think. This is a much friendlier way to describe indentation hinting than making a grammar author maintain an ever-more-complex set of regular expressions. It allows the author to describe each kind of indentation hint as its own rule.
And it allows us to do some more complex things that weren\u2019t possible before.
TextMate\u2019s system will let us indent or dedent one level at a time. But consider a switch
statement:
switch (foo) {
+ case "bar":
+ // one thing
+ break;
+ case "baz":
+ // another thing
+ break;
+ default:
+ // a third thing
+}
+
Between the second-to-last line and the last line, there\u2019s a two-level change in indentation. How can we express that?
Here\u2019s where our syntax tree comes in handy. Instead of describing our desired indentation level relative to the previous line, we can describe it relative to a line of our choosing:
; The closing brace of a switch statement's body should match the indentation
+; of the line where the switch statement starts.
+(switch_statement
+ body: (switch_body "}" @match)
+ (#set! indent.matchIndentOf parent.startPosition))
+
+; 'case' and 'default' need to be indented one level more than their containing
+; \`switch\`.
+(["case" "default"] @match
+ (#set! indent.matchIndentOf parent.parent.startPosition)
+ (#set! indent.offsetIndent 1))
+
+["case" "default"] @indent
+
In the first rule, we\u2019re matching the closing }
of a switch
statement. We\u2019re using a #set!
predicate to describe the starting position of its switch_body
parent. The switch_body
starts on row 1, so @match
understands this to mean \u201Cadjust the current line to match the indentation of row 1.\u201D This will happen automatically when the user types the closing brace.
In the second rule, we\u2019re using similar logic. If we were typing the above switch
statement, we\u2019d find ourselves over-indented as we started typing on line 5. We\u2019d want our editor to dedent that line once it saw that we were typing a new branch of the switch
statement. So we can write another @match
rule \u2014 still targeting the indentation level of the starting row of switch_body
\u2014\xA0but with an extra rule to offset the indent by one level. In other words, we want to be indented one level more than the indent level of row 1.
The third rule is simpler: it\u2019s how we ensure that the editor indents by one level after case
and default
statements.
You might\u2019ve had your hackles raised by this example. After all, there\u2019s another school of thought on how to indent switch
statements:
switch (foo) {
+case "bar":
+ // one thing
+ break;
+case "baz":
+ // another thing
+ break;
+default:
+ // a third thing
+}
+
This faction thinks that case
and default
statements should be indented to the same level as the original switch
statement. How can we please both camps?
One way might be to write two different versions of our second rule, and decide which one to use based on configuration:
+; Some say 'case' and 'default' need to be indented one level more than their
+; containing \`switch\`.
+(["case" "default"] @match
+ (#is? test.config "language-javascript.doubleIndentSwitchStatements")
+ (#set! indent.matchIndentOf parent.parent.startPosition)
+ (#set! indent.offsetIndent 1))
+
+; Others say 'case' and 'default' should match their containing \`switch\`.
+(["case" "default"] @match
+ (#is-not? test.config "language-javascript.doubleIndentSwitchStatements")
+ (#set! indent.matchIndentOf parent.parent.startPosition))
+
Here we\u2019re using a test.config
scope test. I told you about scope tests last time, but I haven\u2019t yet mentioned that they don\u2019t just apply to syntax highlighting queries; they apply to indentation queries, too!
The test.config
scope test gives us a way to approve or reject a capture based on the user\u2019s chosen configuration. If they\u2019ve enabled the doubleIndentSwitchStatements
config option, we can indent their code one way; if they\u2019ve disabled it, we can indent their code another way.
This particular example isn\u2019t yet implemented, but it could be. This would be another advantage that the new Tree-sitter system has over TextMate-style indentation hinting: more room for configurability.
Here\u2019s another edge case of indentation: a braceless if
statement.
How did we pull this off? Haven\u2019t we been targeting nodes like {
and }
?
Yes, but we can also write rules to handle one-line conditionals like these:
(if_statement
+ condition: (parenthesized_expression ")" @indent
+ (#is? test.lastTextOnRow true)))
+
Here we\u2019ve written a query that captures the closing )
of a braceless if
statement and uses that as the indentation hint. We\u2019re also using a scope test to ensure the capture is ignored when it isn\u2019t the last text on the row; that\u2019s how we can avoid a false positive in this scenario:
if (notificationsAreDisabled) return;
+
A braceless if
or else
applies only to the next statement. The real feat is knowing to dedent immediately after that statement:
(if_statement
+ condition: (_) @indent
+ consequence: [
+ (expression_statement)
+ (return_statement)
+ (continue_statement)
+ (break_statement)
+ (throw_statement)
+ (debugger_statement)
+ ] @dedent.next)
+
An if
clause with braces will have a node named statement_block
in its consequence
slot. An if
clause without braces will have its consequence
slot filled with one of these six kinds of nodes instead.
The @dedent.next
capture is only rarely needed, but this is a textbook case: it signals when we should dedent the next line without waiting to see the content of the line. Because we know that the next line should always be dedented in this scenario.
How well does this work? Amazingly well:
Tree-sitter isn\u2019t confused by the line comment! It won\u2019t dedent until after the user types an actual statement.
Does this matter a great deal? Is it worth creating detailed rules to cover a breadth of unusual indentation scenarios? Probably not. But this is just one of a handful of low-hanging fruit that the new system has made possible. Even the slightly verbose query above is much easier to write (and for other people to reason about) than an inscrutable regular expression.
I\u2019m not someone who uses code folding very much, but I want it to be there when I need it. Collapsing entire code branches helps me see the big picture more easily.
Much like indentation, code folding markers in TextMate grammars are determined with regular expressions. Any line that matches foldingStartMarker
is treated as the start of a fold, and any line that matches foldingEndMarker
is treated as the end of a fold. This offers similar tradeoffs to the indentation patterns described above: simple for simple cases, but nearly impossible for complex cases.
(block) @fold
+
That\u2019s the entirety of the contents of the folds.scm
file inside the language-css
package. This works because a starting position and an ending position are all you need to describe a fold, and that\u2019s what Tree-sitter gives us. Every node in a tree reports its buffer positions, so every node can be the target of a fold.
Let\u2019s go a bit deeper and figure out what this does.
When it opens a file, Pulsar needs to figure out where possible fold ranges are so that it can show a small chevron in the gutter on each line where a fold can start. So it\u2019ll run a query capture against each line, testing it to see if any @fold
captures result.
Any results will have their buffer ranges inspected. If the range starts and ends on the same line, the candidate fold is ignored. (This saves grammar authors from having to manually exclude things like one-line conditionals.) But if the range starts on one line and ends on another, the fold is valid, and Pulsar knows to place a chevron in the gutter where the fold would start.
The beginning of a code fold is the very last column on its starting line. In most cases, the range in question will have delimiters on each end \u2014 the backticks of a template string, the curly braces of an if
or else
condition, et cetera. That\u2019s why, by default, the end of a code fold is the starting position of its very last node child. This works as intended in the vast majority of cases, as in our CSS example above:
But because this isn\u2019t always true \u2014 especially for languages like Python that don\u2019t use delimiters for blocks \u2014 we provide ways to tweak a fold\u2019s ending position.
For instance, let\u2019s look at a JavaScript block comment:
Since comment nodes don\u2019t have children, we should set a custom ending position for the fold with fold.endAt
so that it doesn\u2019t try to look up a child node that doesn\u2019t exist. Then we can use fold.offsetEnd
to move the ending point of the fold two columns to the left so that the fold ends before the comment\u2019s ending delimiter:
((comment) @fold
+ (#set! fold.endAt endPosition)
+ (#set! fold.offsetEnd -2))
+
Now we can fold up the block comment the way we want:
Folding in JavaScript is still pretty simple, but not as simple as CSS. We\u2019ve got to account for some edge cases. For example, when an if
statement is followed by an else
, we should adjust the fold so that it ends on the line before the else
, so that each fold can be toggled independently without interfering with one another:
; Target \`if\` consequence blocks that are followed by \`else\`s.
+((if_statement
+ consequence: (statement_block) @fold
+ alternative: (else_clause)
+ (#set! fold.adjustToEndOfPreviousRow true)
+))
+
+; Other \`if\` consequence blocks will get caught here.
+(statement_block) @fold
+
You can see how this works in the screencast below \u2014 the else
block\u2019s closing delimiter folds up to be on the same line as the starting delimiter, but the if
block\u2019s fold stops before the newline.
\u201CEnd the fold at the end of the previous line\u201D is a common enough case to have its own shorthand predicate. We\u2019ve put this special-case query above the simpler one because Pulsar will use the first capture that matches for a given line.
I\u2019ll say it again: this tour through the machinery of Pulsar is aimed at Tree-sitter aficionados and at those who might want to write their own language packages for Pulsar. If that doesn\u2019t describe you, don\u2019t let yourself get overwhelmed by this information dump \u2014 just make note of the new features that this system makes possible.
There are pieces of the indentation and folding systems that I didn\u2019t even try to explain in this post. But all this complexity has a purpose, and users reap the benefits in small increments \u2014 for instance, every time they don\u2019t have to go back and reformat code they paste into an editor.
These systems only work with the assistance of language grammars, so we owe it to the authors of those grammars to hide as much of that complexity as we can. If we can make these systems seem simple on the surface, they\u2019ll work better, and users will be happier.
Cue up your DVD of Inception! Next time we\u2019re delving into language injections \u2014 the feature that lets you write CSS inside of HTML inside of JavaScript inside of HTML inside of PHP.
',26);function Q(K,X){const d=o("RouterLink"),s=o("ExternalLinkIcon");return c(),h("div",null,[t("p",null,[e("Last time we looked at Tree-sitter\u2019s query system and showed how it can be used "),n(d,{to:"/blog/20231013-savetheclocktower-modern-tree-sitter-part-3.html"},{default:u(()=>[e("to make a syntax highlighting engine in Pulsar")]),_:1}),e(". But syntax highlighting is simply the most visible of the various tasks that a language package performs.")]),S,p(" more "),j,t("p",null,[t("a",P,[e("@mauricioszabo"),n(s)]),e(" had noticed "),t("a",H,[e("Neovim\u2019s prior art here"),n(s)]),e(", and had started to implement a similar system, so it was easy to pick up where he left off.")]),M,t("p",null,[e("If you\u2019re following along with "),t("a",A,[L,n(s)]),e(", you can visualize it:")]),t("ol",null,[t("li",null,[e("First, "),t("a",C,[e("make sure you\u2019ve enabled modern Tree-sitter"),n(s)]),e(".")]),B,R,W,E]),N,t("p",null,[e("Here we\u2019re using a new capture called "),O,e(". It can do exactly what we just described by using a "),z,e(" (an idea we introduced "),t("a",Y,[e("in the last installment"),n(s)]),e(") to refer to an earlier line.")]),D,t("p",null,[e("Legacy Tree-sitter grammars introduced a system for defining folds "),t("a",F,[e("within the grammar definition file itself"),n(s)]),e(" \u2014\xA0one that hooks into named nodes from the tree. It\u2019s a good start, but we can make something more expressive and flexible with queries.")]),J,t("p",null,[e("Again, credit goes to the "),V,e(" developers and to "),t("a",U,[e("@mauricioszabo"),n(s)]),e(" for envisioning how Tree-sitter queries can describe folds more simply. Here\u2019s how simple it can be in a language like CSS:")]),G])}const $=l(I,[["render",Q],["__file","20231031-savetheclocktower-modern-tree-sitter-part-4.html.vue"]]);export{$ as default}; diff --git a/assets/20231109-Daeraxa-NovemberUpdate.html.38101774.js b/assets/20231109-Daeraxa-NovemberUpdate.html.38101774.js new file mode 100644 index 0000000000..1e4d59b475 --- /dev/null +++ b/assets/20231109-Daeraxa-NovemberUpdate.html.38101774.js @@ -0,0 +1 @@ +import{_ as r}from"./tools.52491106.js";import{_ as s}from"./detective.e7e89ed0.js";import{_ as i}from"./tree-sitter.6a39e323.js";import{_ as h}from"./spotlight.aba19e76.js";import{_ as l,o as c,c as d,e as p,a as t,b as e,d as n,f as a,r as u}from"./app.0e1565ce.js";const m="/assets/computer.f3068a14.png",g="/assets/pulsar-runner.3044a99a.png",f={},_=t("p",null,"What month is most important for a Pulsar? A supernova-ember! ...What isn't a bad joke is this, the Pulsar community update!",-1),b=a('Another warm welcome to the Pulsar community update, where we cover new developments and events in the world of Pulsar. This month we have a couple of really significant changes to how Pulsar works internally by creating a couple of new APIs that can be used throughout the application, a new package to help you run code directly within Pulsar and our usual community spotlight to say thank you to those community members contributing to Pulsar's development!
UI
APIBy removing all these independent implementations across various core packages, we should be able to reap the following benefits:
atom.io
URLs to the pulsar-edit.dev
or web archive equivalent.Of course this new API will also be available for use by community packages for developers to take advantage of if they wish, so that Markdown parsing doesn't need to be independently implemented any more.
This new feature is availble in our newest rolling releases and will soon be in our regular release too!
fuzzy-native
APIWhat month is most important for a Pulsar? A supernova-ember! ...What isn't a bad joke is this, the Pulsar community update!
\\n","headers":[{"level":2,"title":"New UI API","slug":"new-ui-api","link":"#new-ui-api","children":[]},{"level":2,"title":"Exposing a fuzzy-native API","slug":"exposing-a-fuzzy-native-api","link":"#exposing-a-fuzzy-native-api","children":[]},{"level":2,"title":"A new code runner package","slug":"a-new-code-runner-package","link":"#a-new-code-runner-package","children":[]},{"level":2,"title":"Further installments in the modern Tree-sitter blog post series","slug":"further-installments-in-the-modern-tree-sitter-blog-post-series","link":"#further-installments-in-the-modern-tree-sitter-blog-post-series","children":[]},{"level":2,"title":"Community spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[]}],"git":{"updatedTime":1699577764000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":3.85,"words":1156},"filePathRelative":"blog/20231109-Daeraxa-NovemberUpdate.md","localizedDate":"November 9, 2023"}`);export{e as data}; diff --git a/assets/20231110-savetheclocktower-modern-tree-sitter-part-5.html.bb998452.js b/assets/20231110-savetheclocktower-modern-tree-sitter-part-5.html.bb998452.js new file mode 100644 index 0000000000..48013ee9ea --- /dev/null +++ b/assets/20231110-savetheclocktower-modern-tree-sitter-part-5.html.bb998452.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-168f28ae","path":"/blog/20231110-savetheclocktower-modern-tree-sitter-part-5.html","title":"Modern Tree-sitter, part 5: injections","lang":"en-US","frontmatter":{"title":"Modern Tree-sitter, part 5: injections","author":"savetheclocktower","date":"2023-11-10T00:00:00.000Z","category":["dev"],"tag":["modernization","tree-sitter"]},"excerpt":"One annoying thing that software developers do is insist on writing in more than one language at once. Web developers are espeically obnoxious about this \u2014 routinely, for instance, putting CSS inside their HTML, or HTML inside their JavaScript, or CSS inside their HTML inside their JavaScript.
\\nCode editors like Pulsar need to roll with this, so today we\u2019ll talk about how the modern Tree-sitter system handles what we call injections.
\\n","headers":[{"level":2,"title":"A mental model for injections","slug":"a-mental-model-for-injections","link":"#a-mental-model-for-injections","children":[{"level":3,"title":"Language layers","slug":"language-layers","link":"#language-layers","children":[]}]},{"level":2,"title":"How Tree-sitter envisions injections","slug":"how-tree-sitter-envisions-injections","link":"#how-tree-sitter-envisions-injections","children":[]},{"level":2,"title":"How Pulsar implements injections","slug":"how-pulsar-implements-injections","link":"#how-pulsar-implements-injections","children":[{"level":3,"title":"How does it know which language to use?","slug":"how-does-it-know-which-language-to-use","link":"#how-does-it-know-which-language-to-use","children":[]}]},{"level":2,"title":"Architecture","slug":"architecture","link":"#architecture","children":[{"level":3,"title":"Challenges","slug":"challenges","link":"#challenges","children":[]}]},{"level":2,"title":"Strange injection tricks","slug":"strange-injection-tricks","link":"#strange-injection-tricks","children":[{"level":3,"title":"\u201CRedaction\u201D in injections","slug":"redaction-in-injections","link":"#redaction-in-injections","children":[]},{"level":3,"title":"Macros in Rust/C/C++","slug":"macros-in-rust-c-c","link":"#macros-in-rust-c-c","children":[]},{"level":3,"title":"Injecting highlighting for URLs and TODOs","slug":"injecting-highlighting-for-urls-and-todos","link":"#injecting-highlighting-for-urls-and-todos","children":[]},{"level":3,"title":"Markdown and front matter","slug":"markdown-and-front-matter","link":"#markdown-and-front-matter","children":[]}]},{"level":2,"title":"Next time","slug":"next-time","link":"#next-time","children":[]}],"git":{"updatedTime":1699689463000,"contributors":[{"name":"Andrew Dupont","email":"github@andrewdupont.net","commits":6}]},"readingTime":{"minutes":16.04,"words":4812},"filePathRelative":"blog/20231110-savetheclocktower-modern-tree-sitter-part-5.md","localizedDate":"November 10, 2023"}');export{e as data}; diff --git a/assets/20231110-savetheclocktower-modern-tree-sitter-part-5.html.c7b10bdf.js b/assets/20231110-savetheclocktower-modern-tree-sitter-part-5.html.c7b10bdf.js new file mode 100644 index 0000000000..e0c8b198b8 --- /dev/null +++ b/assets/20231110-savetheclocktower-modern-tree-sitter-part-5.html.c7b10bdf.js @@ -0,0 +1,132 @@ +import{_ as o,o as i,c as p,e as c,a,b as n,d as s,f as e,r as l}from"./app.0e1565ce.js";const r="/assets/tree-sitter-tools-style-element.6c7c3556.png",u="/assets/tree-sitter-injection-illustration.c0de6ada.png",d="/assets/tree-sitter-tools-tree-list.7475c4eb.png",h="/assets/tree-sitter-select-larger-syntax-node.522d277a.webm",k="/assets/tree-sitter-select-larger-syntax-node.07d04699.mp4",g="/assets/tree-sitter-tools-template-string.34be245b.png",m="/assets/tree-sitter-html-parser-injection.b2a40e1c.png",v="/assets/tree-sitter-todo-url-injection.231723ac.png",f={},b=a("p",null,[n("One annoying thing that software developers do is insist on writing in "),a("em",null,"more than one language at once"),n(". Web developers are espeically obnoxious about this \u2014 routinely, for instance, putting CSS inside their HTML, or HTML inside their JavaScript, or CSS inside their HTML inside their JavaScript.")],-1),y=a("p",null,[n("Code editors like Pulsar need to roll with this, so today we\u2019ll talk about how the modern Tree-sitter system handles what we call "),a("em",null,"injections"),n(".")],-1),w=e(`The TextMate grammar system understands injections. In any context, a TextMate grammar can include a subset of its own rules\u2026 or an entirely separate grammar.
But Tree-sitter needs something a bit more elaborate. If I\u2019ve got CSS inside a style
tag in my HTML file, now I\u2019ve got two different parsers, each responsible for a different range of code. If I make some changes inside that style
block, both parsers need to react to it.
Injections cover a wide range of use cases \u2014\xA0from the examples above to fenced code blocks in Markdown files to special-purpose injections that recognize things like URLs. Injections allow us to do some powerful and useful things that would be hard to do otherwise \u2014 including some things that TextMate injections can\u2019t do at all.
Let\u2019s pretend we have a simple HTML file that looks like this:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title>Sample</title>
+
+ <style>
+ body {
+ padding: 0;
+ }
+ </style>
+
+ <script type="text/javascript">
+ window.dataLayer = window.dataLayer || [];
+ function gtag() {
+ dataLayer.push(arguments);
+ }
+ gtag("js", new Date());
+ gtag("config", "G-ABCDEFGHIJ");
+ </script>
+ </head>
+ <body></body>
+</html>
+
We haven\u2019t gotten very close to the machinery so far in this series, but I\u2019ve been content to have you model this as a single document with a single Tree-sitter HTML parser responsible for all syntax highlighting. That works fine until we get to the contents of the style
and script
elements.
To the Tree-sitter HTML parser, a style
element looks like this:
You can see that it performs the usual parsing on the start and end tags, but punts on parsing the CSS itself \u2014 instead marking it as raw_text
. This is what it should do! It\u2019s not a CSS parser, after all. It treats the inline script
element similarly, marking its contents as raw_text
because it doesn\u2019t understand JavaScript.
To apply syntax highlighting to these areas, we need to bring in parsers that understand these languages.
So our mental model needs to evolve. Instead of one buffer with one parser, we have one buffer with three parsers. We need a name for \u201Ca region of the buffer that uses a specific grammar to be understood,\u201D so let\u2019s call it a language layer, because that\u2019s what Pulsar calls it under the hood.
Imagine a simpler HTML file that doesn\u2019t have any inline style
or script
tags:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title>Sample</title>
+ </head>
+ <body></body>
+</html>
+
Here we\u2019re looking at a buffer with a single language layer at the root. When I type a new keystroke in this buffer, only one Tree-sitter parser has to do any re-parsing work, and only one layer needs to be consulted when re-applying syntax highlighting.
Once I add a style
block\u2026
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title>Sample</title>
+
+ <style>
+ body {
+ padding: 0;
+ }
+ </style>
+ </head>
+ <body></body>
+</html>
+
\u2026I trigger the creation of a second language layer. This new layer is a child of the root HTML layer \u2014\xA0because the HTML layer is its reason for being, and the CSS layer might go away in the future if the HTML layer changes. The new language layer uses a Tree-sitter CSS parser.
When I add back the script
block\u2026
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title>Sample</title>
+
+ <style>
+ body {
+ padding: 0;
+ }
+ </style>
+
+ <script type="text/javascript">
+ window.dataLayer = window.dataLayer || [];
+ function gtag() {
+ dataLayer.push(arguments);
+ }
+ gtag("js", new Date());
+ gtag("config", "G-ABCDEFGHIJ");
+ </script>
+ </head>
+ <body></body>
+</html>
+
\u2026I trigger the creation of another language layer: a JavaScript layer that is also a child of the root HTML layer. Now there are three layers that might need to be consulted for syntax highlighting and other tasks.
And it doesn\u2019t stop here! Certain constructs inside of the JavaScript, like regular expressions or tagged template literals, might carry their own injections, in which case new language layers would be created as children of the JavaScript layer. The result is a tree of language layers which cooperate to apply syntax highlighting to our buffer.
This might feel impossibly complex, but it isn\u2019t. It\u2019s just a different approach to what was already being done with TextMate grammars. In a minute I\u2019ll explain how Pulsar manages this complexity.
((script_element
+ (raw_text) @injection.content)
+ (#set! injection.language "javascript"))
+
+((style_element
+ (raw_text) @injection.content)
+ (#set! injection.language "css"))
+
list_items.each do |item|
+ puts <<~HTML
+ <li>#{item.name}</li>
+ HTML
+end
+
This is a thorough system, and we\u2019ve already shown how query files can solve problems like syntax highlighting and injection hinting. So it can solve our language injection problem, right?
The stuff I just described makes a lot of sense, and it\u2019s possible we\u2019ll do it someday. But here\u2019s how Pulsar does it now:
atom.grammars.addInjectionPoint("text.html.basic", {
+ type: "script_element",
+ language() {
+ return "javascript";
+ },
+ content(node) {
+ return node.child(1);
+ },
+});
+
+atom.grammars.addInjectionPoint("text.html.basic", {
+ type: "style_element",
+ language() {
+ return "css";
+ },
+ content(node) {
+ return node.child(1);
+ },
+});
+
This code block is the equivalent of the above query file for HTML injections. It\u2019s got a similar amount of flexibility for selecting a target (e.g., \u201Cthe second child of a style_element
node\u201D) and the ability to determine the language name either dynamically or statically. But we\u2019ve done everything else via queries; why do we do injections this way instead?
The addInjectionPoint
API was added by the legacy Tree-sitter implementation. For reasons of continuity, it makes a lot of sense to keep using this API rather than switch to something that\u2019s functionally the same.
In fact, there\u2019s one thing that the addInjectionPoint
API does that a hypothetical injections.scm
file can\u2019t: it can be used to add injections to Language X even by someone who doesn\u2019t control the language-x
bundle. This makes it far more useful to Pulsar! It means that someone can write their own parser that injects itself into another language, whether in the form of a community package or a few lines in a user\u2019s init.js
.
To me, it doesn\u2019t make sense to deprecate the addInjectionPoint
approach when it can do things that a query-based approach can\u2019t. Still, lots of Tree-sitter parsers include that query file, so I imagine that Pulsar will eventually support it in addition to the addInjectionPoint
API.
You might\u2019ve noticed that both the Tree-sitter query file example and our addInjectionPoint
example refer to the to-be-injected language rather casually \u2014 javascript
and css
. Internally, grammars tend to refer to one another via their root scope \u2014 as in the text.html.basic
case above. So why not just use the root scope?
Two reasons:
`,11),S=e("javascript
instead of source.js
\u2014 because more than one grammar could theoretically identify as a JavaScript grammar, and because a grammar might want to respond to more than one name. (js
and javascript
, c++
and cpp
, and so forth.)The first item in the list will always be the \u201Croot\u201D tree. Other items, if present, represent injections. And because injected languages can have their own injections, this list can grow to arbitrary length.
When the user edits the buffer, even with a single keystroke, we re-parse the document from the root down as follows:
addInjectionPoint
.addInjectionPoint
(does the injection describe a language whose name we can match to a grammar? does the node specified by the content
callback exist?), then we try to match them up to the layers that already exist. Layers that can\u2019t be matched to current injection nodes are disposed of, and nodes that can\u2019t be matched to existing layers get new layers created for them.Any keystroke can create a brand new injection or invalidate one that used to exist. If I put the cursor inside of <style>
and insert an x
, changing it to <styxle>
, then the CSS injection would no longer exist, and its corresponding LanguageLayer
instance would need to be destroyed. If I then undo my change, the parse tree restores the style_element
node, and a new language layer is created.
Does this feel overwhelming? That\u2019s fair. After all, I\u2019m writing this blog post in a buffer with 32 different language layers across the various code examples, and you\u2019d think that would add up to one hell of a performance penalty on each keystroke. But it doesn\u2019t.
Here are a few reasons why:
We don\u2019t revisit every single language layer on every single keystroke because we can determine when a given buffer change cannot possibly affect a given injection. For instance, if I\u2019m editing a section of my HTML file outside of the style
block, then Pulsar knows it doesn\u2019t have to re-parse the CSS inside of that style
block yet. It knows that the layer\u2019s parse tree, though technically stale, is not invalid, and will defer re-parsing until an edit happens within its extent of the buffer. (This is true even if those edits cause the style
block to shift its position in the document!) As a result, lots of buffer changes can short-circuit the exhaustive process I just described.
Syntax highlighting in particular is designed for performance, even in injection scenarios. After a buffer\u2019s initial highlighting pass, a given section of code will retain its highlighting indefinitely, even if its position in the buffer shifts as the result of other edits.
Syntax re-highlighting only happens when a buffer range is specifically invalidated. When an edit happens, Tree-sitter tells us how that edit affects the syntax tree, which in turn tells us which parts of the buffer need to be re-highlighted \u2014 and, just as importantly, which parts don\u2019t need to be re-highlighted.
Tree-sitter is faster than you think it is. The smaller the edit, the more the parser can reuse its old work, and the faster the re-parse happens.
Hardly anything in this process happens synchronously, so buffer operations will feel fast even in the rare case where Tree-sitter needs time to catch up.
Annoyingly, the answer is different for each system. For instance:
So sometimes we need to aggregate across layers, but other times we need to pick a winner.
Picking a winner is the obvious approach for indentation when you think it through. If I hit Return when the cursor is in a script
block, then I\u2019m writing JavaScript, and the JavaScript layer should be the one making indentation decisions. More generally, this means that if more than one layer is active at a given buffer position, we should pick the deepest layer and ask it to decide. (Sometimes this means the deepest layer that fulfills a certain criterion \u2014\xA0in this case, the deepest layer that actually defines an indentation query.)
But aggregating is the obvious approach for other scenarios. Tree-sitter grammars get to support Pulsar\u2019s Editor: Select Smaller Syntax Node and Editor: Select Larger Syntax Node commands (you don\u2019t know you need them in your life until you give them a try!) and those commands should work properly across injection boundaries. So when either command is invoked with the cursor at a given position, we should figure out which nodes contain that point regardless of which parse tree owns them. Then we can arrange those nodes from smallest to largest.
You can see the results here. As I expand the selection by invoking Select Larger Syntax Node over and over, the selection starts with nodes in the CSS injection, jumps to nodes in the parent JavaScript injection, then jumps again to nodes in the root HTML injection.
Mixing languages in a single buffer is messy, so injections need some unusual features in order to deal with that messiness. These features can be used in surprising and powerful ways.
One thing that makes Tree-sitter injections more powerful than their TextMate equivalents is their ability to ignore content that isn\u2019t relevant to their jobs. The injection engine \u201Credacts\u201D all content except what it wants a given layer to see.
Suppose you had an html
tagged template literal:
let markup = html` <p>Copyright 2020\u20132023 John Doe</p> `;\n
Since the tag hints at the language name, Pulsar will give you HTML syntax highlighting for free inside the literal. But that literal is still JavaScript, so what happens if we do this?
let now = new Date();\nlet markup = html` <p>Copyright 2020\u2013${now.getFullYear()} John Doe</p> `;\n
That ${now.getFullYear()}
part isn\u2019t actually HTML. This example won\u2019t confuse an HTML parser, so it\u2019s not a big deal; but there does exist valid content inside of a template interpolation that definitely would flummox the injection:
let evil = html` <p>this might not ${"</p>"} get parsed correctly</p> `;\n
Ideally, the HTML injection wouldn\u2019t see that interpolation at all. So what if we could hide it?
We can. In fact, we do! Here\u2019s what that template string looks like in tree-sitter-tools
:
Our injection is defined such that we specify the template_string
node as the content. That means Pulsar will use the buffer range of that node, but will subtract the ranges of any of the node\u2019s children!
We can visualize this with the \u201CShow injection ranges\u201D option in tree-sitter-tools
:
You can see that the HTML injection layer has two disjoint content ranges on either side of the interpolation. The Tree-sitter HTML parser won\u2019t even know the interpolation is there.
This behavior makes sense as a default, but it can be opted out of with includeChildren: true
in addInjectionPoint
if it gets in your way.
content
callbackA grammar author has another tool to control what gets redacted: the content
callback. It\u2019s not limited to returning a single node! It can return an array of nodes, each with its own range; there\u2019s no obligation for those ranges to be adjacent.
Our first HTML injection example earlier applied its own subtle redaction. We specified a type
of script_element
, but a content
callback that returns that element\u2019s second child (the raw_text
node). So the type
property tells Pulsar which node to query for (create an injection for each script
element) but content
selects the node(s) that will be meaningful to the parser (omit the <script>
and </script>
because those aren\u2019t JavaScript).
This flexibility means that it\u2019s possible for all your injections of a certain type to share one language layer. Instead of creating one layer for each of N different buffer ranges, you could create one layer with N disjoint content ranges. The trade-off is that an injection that covers more of the buffer will need to be re-parsed more often in response to buffer changes, but that trade-off might make sense in certain scenarios.
C, C++, and Rust allow you to define macros via a preprocessor. Macros are weird for a parser: they can\u2019t be parsed as though they\u2019re valid source code, because they might be fragments of code that aren\u2019t syntactically valid until after preprocessing.
Hence they\u2019re a situation where a language might want to inject itself into itself. Consider this code example:
#define circleArea(r) (3.1415*(r)*(r))
+
The #define
keyword and the circleArea(r)
preprocessor function signature have to be well-formed, but everything that follows is an anything-goes nightmare for a syntax highlighter. The preprocessor won\u2019t try to parse it or make it make sense; it\u2019ll just make the appropriate substitution throughout the source file and enforce validity later.
For the same reason, the tree-sitter-c
parser doesn\u2019t attempt to do any parsing of the preprocessor argument \u2014 the (3.1415*(r)*(r))
in our above example. But that argument will often be valid C, so there\u2019s no reason why we shouldn\u2019t take a stab at it:
atom.grammars.addInjectionPoint(\`source.c\`, {
+ type: "preproc_function_def",
+ language(node) {
+ return "c";
+ },
+ content(node) {
+ return node.lastNamedChild; // the \`preproc_arg\` node
+ },
+});
+
This is a low-stakes gambit for us. If the content of the macro is syntactically strange, the parser might get a bit flummoxed, and the resulting highlighting might look a bit weird. But that\u2019s OK! It won\u2019t affect the highlighting of anything outside of the macro content.
Two built-in packages called language-todo
and language-hyperlink
define specialized TextMate grammars. Their purpose is to provide rules that match TODO:
remarks (in comments) and URLs (in comments and strings), and to inject those rules into certain contexts regardless of grammar. This is a lovely feature of TextMate that the Atom developers got for free when implementing TextMate-style grammars back in the day.
The effect is that Pulsar can help you locate TODOs in comments by coloring them differently from the rest of the comment. It can also draw underlines under URLs and even follow a URL when you place your cursor inside of it and invoke the Link: Open command.
This works because a TextMate grammar can \u201Cpush\u201D its injections into any scope inside any other grammar, whether that other grammar asks for it or not. For instance, the language-hyperlink
grammar injects itself into strings, so any language that defines a string.*
scope will have those rules injected into it.
The legacy Tree-sitter system never had an equivalent feature. I missed it terribly, so I decided to create equivalent Tree-sitter parsers and grammars for these rules. These parsers, when given arbitrary text, can create nodes for things that look like URLs or TODO comments. Once those parsers existed, I could inject them into whichever grammars I wanted:
for (let type of ["template_string", "string_fragment", "comment"]) {
+ atom.grammars.addInjectionPoint("source.js", {
+ type,
+ language: () => {
+ return "hyperlink";
+ },
+ content: (node) => node,
+ languageScope: null,
+ });
+}
+
There\u2019s one new thing here: the languageScope
option. Typically, you\u2019ll want a grammar\u2019s base scope name to be present inside of an injection; for instance, you\u2019d want a source.js
scope name to exist inside of an HTML script
block. But that behavior doesn\u2019t make sense for our use case. We want to add a scope name around a URL when it\u2019s present, but otherwise we want to operate stealthily. Passing null
to the languageScope
option lets us bypass the default behavior.
There\u2019s one other thing to address, though. Most comments won\u2019t have URLs in them. Most strings won\u2019t have URLs in them. If I use this code as-is, I\u2019ll be creating one new injection for every string, every line comment, and every block comment in my JavaScript file, whether a URL is present or not. (This unnecessary work, believe it or not, doesn\u2019t create any sluggishness during routine editing, but we should still try to avoid it.)
What should I do? One option would be to do what I described above: create one large injection for the entire document and have it be in charge of all comments and strings in the document. That was my first experiment, but I decided against it because the trade-off wasn\u2019t worth it: incremental re-parses were slower because every buffer change meant that my URL parser had to re-scan the whole buffer.
I\u2019m willing to chalk part of that up to my lack of experience writing Tree-sitter parsers! I\u2019d bet there are things I can do to make those parses less costly. But in the meantime, I applied a Stupid Human Trick\u2122 to get the best of both worlds:
const HYPERLINK_PATTERN = /\\bhttps?:/;
+
+for (let type of ["template_string", "string_fragment", "comment"]) {
+ atom.grammars.addInjectionPoint("source.js", {
+ type,
+ language: (node) => {
+ return HYPERLINK_PATTERN.test(node.text) ? "hyperlink" : undefined;
+ },
+ content: (node) => node,
+ languageScope: null,
+ });
+}
+
I can assure you that this feels incredibly silly to do, but it works: we\u2019re pre-screening the content of the node and ignoring those that definitely don\u2019t contain a URL. Returning undefined
from the language
callback prevents a layer from being needlessly created. We employ a similar strategy for the TODO
highlighting.
There\u2019s another thing that feels awkward about this: it\u2019s not as automatic as the previous TextMate solution. Instead of being able to \u201Cpush\u201D these injections into other grammars, we\u2019re asking those grammars to \u201Cpull\u201D our injections into themselves.
In an ideal world, I\u2019d be able to create a generalized injection that applied to all files as easily as in the TextMate grammars. But to create a Tree-sitter injection I\u2019ve got to describe the name of the node I want to inject into. And there aren\u2019t many safe assumptions you can make about Tree-sitter node naming conventions.
The saving grace here is what I mentioned above: the injection API lets you inject things into someone else\u2019s language grammar. So if your favorite community language package doesn\u2019t highlight TODOs and URLs, you can fix that with about six lines of JavaScript in your init.js
.
We hope you enjoyed reading about this update as much as we hope you continue to enjoy using Pulsar. As ever, a huge thank you to our generous donors and community, without whom this project would not be possible.
Until next time, happy coding, and see you amongst the stars!
atom
, accessible via atom.ui
. This exposes a markdown
object, allowing community packages to offload Markdown handling to the core editor.'...'
) and C-style ($'...'
) strings in shell scripts.pulsar-updater
package).Pulsar 1.111.0
If you have been good this year, you might just find something in your stocking this Christmas. Until then, you can find the Pulsar community update right here!
\\n","headers":[{"level":2,"title":"Update on Electron updates","slug":"update-on-electron-updates","link":"#update-on-electron-updates","children":[{"level":3,"title":"Tree-sitter","slug":"tree-sitter","link":"#tree-sitter","children":[]},{"level":3,"title":"Making it available","slug":"making-it-available","link":"#making-it-available","children":[]},{"level":3,"title":"Package compatibility","slug":"package-compatibility","link":"#package-compatibility","children":[]}]},{"level":2,"title":"PPR website issues","slug":"ppr-website-issues","link":"#ppr-website-issues","children":[]},{"level":2,"title":"Community Spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[{"level":3,"title":"Default file icons","slug":"default-file-icons","link":"#default-file-icons","children":[]}]}],"git":{"updatedTime":1702350476000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":3.45,"words":1036},"filePathRelative":"blog/20231212-Daeraxa-DecemberUpdate.md","localizedDate":"December 11, 2023"}');export{e as data}; diff --git a/assets/20231212-Daeraxa-DecemberUpdate.html.6af4fa5b.js b/assets/20231212-Daeraxa-DecemberUpdate.html.6af4fa5b.js new file mode 100644 index 0000000000..f74e74c9dc --- /dev/null +++ b/assets/20231212-Daeraxa-DecemberUpdate.html.6af4fa5b.js @@ -0,0 +1 @@ +import{_ as r}from"./electron.b5867f08.js";import{_ as i}from"./tools.52491106.js";import{_ as s}from"./spotlight.aba19e76.js";import{_ as h,o as l,c,e as d,a as e,b as t,d as o,f as n,r as u}from"./app.0e1565ce.js";const p="/assets/file-icons.45aa1c32.png",m={},f=e("p",null,"If you have been good this year, you might just find something in your stocking this Christmas. Until then, you can find the Pulsar community update right here!",-1),g=n('Another warm welcome to the Pulsar community update, where we cover new developments and events in the world of Pulsar. This month we have a couple of really significant changes to how Pulsar works internally by creating a couple of new APIs that can be used throughout the application, a new package to help you run code directly within Pulsar and our usual community spotlight to say thank you to those community members contributing to Pulsar's development!
On to our GitHub package, we found that we had a rather common issue with people not being able to log into their GitHub account via the package. Essentially, it was possible to set scopes in such a way that, although permissions were technically granted, Pulsar was unable to read the scopes and refused to log in. To solve this, we have updated the package to provide feedback and improved the scope checking logic. We also updated the link to the Personal Access Token page to include by default the scopes that Pulsar requires.
Continuing with the theme of last month's new "UI" API, we have another new API that all packages can now take advantage of. This time it is ui.fuzzyMatcher
which will allow packages to use Pulsar's fuzzy-finder
module without needing to bundle it into their own packages.
And to finish off with a bug fix, an issue was found where Pulsar wasn't correctly inheriting the directory from which the pulsar
binary was being run, leading to some slightly odd behaviour.
It is hard to believe that it has been an entire year since we created our first tagged release of Pulsar and we never would have managed to get to this milestone without the amazing support from our donors and our community, so as ever, a massive thank you to everyone who has allowed us to get this far!
Until next time (and next year!), happy coding, and see you amongst the stars!
atom.ui.fuzzyMatcher
API, moving the Pulsar fuzzy-finder
module into the core of the editor for community packages to utilize.pulsar
binary was run.Pulsar 1.112.0
Hotfix: Pulsar 1.112.1 is available now!
First of all, we hope everyone had a fantastic new year and here is to a new one! While the Pulsar team has been largely enjoying the holidays we still have plenty of things to update you with!
Happy new year! Welcome to the first Pulsar community update of 2024!
\\n","headers":[{"level":2,"title":"New tree-sitter becoming default","slug":"new-tree-sitter-becoming-default","link":"#new-tree-sitter-becoming-default","children":[]},{"level":2,"title":"Annoying website bug found and zapped!","slug":"annoying-website-bug-found-and-zapped","link":"#annoying-website-bug-found-and-zapped","children":[]},{"level":2,"title":"New PPR owners/:ownerName API endpoint","slug":"new-ppr-owners-ownername-api-endpoint","link":"#new-ppr-owners-ownername-api-endpoint","children":[]},{"level":2,"title":"Long-term projects","slug":"long-term-projects","link":"#long-term-projects","children":[]},{"level":2,"title":"Community Spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[]}],"git":{"updatedTime":1705202015000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":2.64,"words":793},"filePathRelative":"blog/20240112-Daeraxa-JanuaryUpdate.md","localizedDate":"January 12, 2024"}');export{e as data}; diff --git a/assets/20240115-Daeraxa-v1.113.0.html.1359019c.js b/assets/20240115-Daeraxa-v1.113.0.html.1359019c.js new file mode 100644 index 0000000000..438a5de46a --- /dev/null +++ b/assets/20240115-Daeraxa-v1.113.0.html.1359019c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3500d176","path":"/blog/20240115-Daeraxa-v1.113.0.html","title":"Unlucky for some, but not us. Our 13th release, Pulsar 1.113.0, is available now!","lang":"en-US","frontmatter":{"title":"Unlucky for some, but not us. Our 13th release, Pulsar 1.113.0, is available now!","author":"Daeraxa","date":"2024-01-15T00:00:00.000Z","category":["dev"],"tag":["release"]},"excerpt":"Pulsar 1.113.0
And that is all we have for you right now. We hope you enjoy this new release and here is to another 12 months of Pulsar!
Until next time, happy coding, and see you amongst the stars!
github
package's diff views.symbols-view
package to accept symbols from a number of sources, including Tree-sitter grammars and IDE packages.symbols-view
when returning from visiting a symbol declaration.We\u2019ve been telling a series of stories about all the different ways that Tree-sitter can improve the editing experience in Pulsar. Today\u2019s story about symbols-view
starts a bit slowly, but it\u2019s got a great ending:\xA0the addition of a major new feature to Pulsar 1.113.
render
, andChoosing the render
symbol in your symbols list will move the editor to the line where render
is defined.
You might have noticed how GitHub can nowadays give you an outline-like view of a source code file, listing lines where methods are defined. That\u2019s all happening through Tree-sitter. If GitHub can use it for symbol navigation, so can we.
symbols-view
But to make that happen, we need to change how symbols-view
works. All it knows about is ctags
! Could we rip all that out and replace it with a Tree-sitter solution? Yes, but in the process we\u2019d be abandoning support for any languages that don\u2019t yet have Tree-sitter parsers.
A better approach would be to know about both strategies and pick the best one on the fly. So let\u2019s figure out exactly what Maur\xEDcio\u2019s request \u2014 \u201Ctransforming [symbols] into a \u2018service\u2019\u201D \u2014 means.
In Pulsar, services are how packages talk to one another. Suppose I\u2019ve authored package-b
and it depends on another package called package-a
that someone else has written. I could reach into atom.packages
and grab the reference to package-a
, but this feels weird for a number of reasons. For one, it incorrectly assumes that package-a
has already been activated. It may get activated after package-b
\u2014 or else it may never get activated because the user has disabled or uninstalled it.
But even if package-b
were able to find and consume package package-a
this way, it\u2019d create a tight coupling between the two. That coupling would break if package-a
renamed itself, or if it changed implementation details that package-b
was relying on.
So instead of communicating directly, they can invent a service called foo
and use it to communicate. One package defines itself as a provider of service foo
, and the other defines itself as a consumer of service foo
.
During startup, Pulsar will activate each package, notice the match, and arrange an introduction as soon as both packages have been activated. The provider will end up returning an object that the consumer can use however it likes; this object is typically some sort of interface with methods that the consumer can call.
Services thus act as contracts between packages. And they can be versioned, too. If it wants, package-a
can provide several different versions of the service at once; this leaves the author free to make changes without breaking packages that consume the older version.
This flexibility makes new things possible. Consider a package like autocomplete-plus
, the bundled package that provides an autocompletion menu in Pulsar. It doesn\u2019t try to implement the various tactics that can be used to suggest completion candidates; all it does is make the user interface for an autocomplete menu. It then defines an autocomplete.provider
service so that other packages can provide completion suggestions. Packages like autocomplete-html
, autocomplete-css
, and others know how to suggest context-specific completions at the cursor, so they feed that data to autocomplete-plus
.
We like this approach because it gives users an incredible amount of control. For example, if you don\u2019t like the HTML autocompletion suggestions, you can change autocomplete-html
\u2019s configuration, or even disable it entirely. Or you could write your own alternative to autocomplete-html
. Or you could even write your own alternative to autocomplete-plus
! By registering as a consumer of autocomplete.provider
, your replacement package would be able to communicate with packages like autocomplete-html
just as easily as autocomplete-plus
can.
This is the model we need for symbols-view
. We now have a second approach for generating symbols that can compete favorably with the ctags
strategy. So let\u2019s reinvent symbols-view
in the style of autocomplete-plus
and make it a consumer of a new service we\u2019ll invent named symbol.provider
.
The built-in ctags
provider can be spun off into a package called symbol-provider-ctags
, and our new Tree-sitter\u2013based approach can be in a package called symbol-provider-tree-sitter
. These packages can provide the symbol.provider
service for symbols-view
to consume.
When a user presses Ctrl+R / Cmd+R, we can run a query against the current buffer. Any node that is captured as @name
in a tags.scm
file can be represented as a symbol. Often the node will be contained in a larger capture called (for example) @definition.function
; we can detect that and infer that the text captured by @name
refers to a function.
The information we get is not only richer than what ctags
can provide, but also more accurate, since we\u2019re querying against the current buffer text. Even if the file hasn\u2019t been saved recently. Even if it hasn\u2019t been saved at all!
Now, we can only do this when the file in question is using a Tree-sitter grammar, so it\u2019s not a universal solution. But we can prefer a Tree-sitter symbol provider where it\u2019s available, and fall back to our ctags
provider where it isn\u2019t.
Another thing that symbols-view
has long supported \u2014 theoretically \u2014 is project-based symbol navigation, allowing you to search for and jump to symbols in other files.
It\u2019s been able to do this because ctags
can read project-wide symbol metadata \u2014 a genuine upside it has over some other approaches. But this feature only works if the user has generated a special file called a \u201Ctags file\u201D for their project. Pulsar itself can\u2019t generate this file on its own because it doesn\u2019t know which files it should crawl to find symbols (imagine if it tried to crawl your entire node_modules
folder!), so the ctags
strategy requires the user to regenerate that file on a regular basis.
For now, our Tree-sitter symbol provider can only suggest symbols in the current file. If you activate Toggle Project Symbols via Ctrl+Shift+R / Cmd+Shift+R, it won\u2019t even volunteer for the job. Using Tree-sitter to list the symbols in an open buffer is very fast precisely because the buffer is open; we\u2019ve already paid the startup cost of the initial parse. But there\u2019s no way Tree-sitter could parse all of a project\u2019s files in a similar amount of time. If we want project-wide symbol search we\u2019ll have to look elsewhere.
\u201CWho cares,\u201D you may think. And I\u2019ll admit I don\u2019t attempt a project-wide symbol search very often. But there\u2019s a related feature I\u2019m pretty sure you\u2019ll like.
symbols-view
defines a Go To Declaration command. It\u2019ll search the project for a symbol matching the word under the cursor. If there\u2019s one result, it\u2019ll get opened automatically; if there\u2019s more than one, it offers up the options in a list for you to choose. And when you\u2019re done, there\u2019s a corresponding Return From Declaration command that takes you back to the place you just were.
Dive into a definition with Ctrl+Alt+Down / Cmd+Alt+Down, then return to the surface with Ctrl+Alt+Up / Cmd+Alt+Up:
Here I\u2019ve demonstrated it on a TypeScript type, but it\u2019ll work on functions and classes and other types of things, too.
Did you know this feature existed? I didn\u2019t. It\u2019s been available to you this whole time if you\u2019ve had a tags file to supply project-wide symbols, the way nobody does. But with a refactored symbols-view
, another candidate for supplying these symbols enters the arena: a language server.
And it might take a few version bumps on dependencies, but most other IDE backend packages can also be updated to take advantage of these features.
symbols-view
Unlike autocomplete-plus
, which aggregates suggestions from multiple providers and shows all of them to the user, symbols-view
is mainly interested in choosing the best provider for the job. There\u2019s little point in aggregating across a language server and Tree-sitter and ctags
, since they\u2019re largely going to be offering the same list of symbols with varying degrees of richness, and you\u2019d be pretty annoyed if Pulsar offered you three different list entries for the same function. Inside symbols-view
they\u2019re called \u201Cexclusive\u201D providers because only one of them will be picked for the job.
But I wanted to leave the door open for some creative and unexpected usages, so symbols-view
also has a concept of \u201Csupplemental\u201D providers. A provider that marks itself as supplemental is saying it\u2019d like to contribute symbols that would probably not already be in an exclusive provider\u2019s list. You may be wondering what kinds of symbols would fit the bill, so let me give you an example\u2026
Did you know you can bookmark lines in a buffer? Try it out: right-click on any line of your editor and select Toggle Bookmark. The built-in bookmarks
package keeps track of them and will also let you navigate between them via F2 and Shift+F2.
I\u2019ve had most of this article written for months, but I decided to wait to publish it until we could show this stuff off. That time is now.
Pulsar 1.113 makes two major changes that will vastly improve the quality of the symbol searching you might already be accustomed to:
The new version of symbols-view
is now in place. It will offer you ctags
-based symbols in grammars that don\u2019t use Tree-sitter, but it will prefer Tree-sitter\u2013supplied symbols in most grammars. If you truly don\u2019t like change, you can disable the symbol-provider-tree-sitter
package and just rely on symbol-provider-ctags
, or else you can configure symbols-view
to prefer some providers over others.
But I\u2019m betting you\u2019ll want to keep using the symbols provided by Tree-sitter, because\u2026
As you may have heard, modern Tree-sitter grammars are now the default! The system that we shipped in experimental fashion back in Pulsar 1.106 is now ready for prime time. For now, you can opt back into legacy Tree-sitter with the new core.useLegacyTreeSitter
setting \u2014 but not for long, because the legacy system will be dropped when we\u2019re finally able to migrate to a newer version of Electron.
How does this actually improve the symbols-view
experience? Let\u2019s see what our original example looks like with a Tree-sitter symbol provider:
The richness of the metadata we get from these sources has allowed us to enhance the symbols-view
UI, too! You\u2019ll be shown the \u201Ckind\u201D of thing that a symbol is \u2014\xA0class, function, constant, et cetera. In many cases, these kinds will be illustrated with icons. Visit the package settings page for symbols-view
to explore the possibilities.
And there are even a few killer new features. Open a symbols list on a JSON file and marvel at the entries you see:
In the beginning, Atom appeared. It created an API to make packages, but together with this API, it also allowed authors to use web APIs together with node.js packages, modules (including "native modules" - more on that later) and, finally, a special API that was used to communicate between the "main module" and the "browser part".
That last part, eventually, split from Atom and became Electron. And for a while, the Atom development was tied to the Electron one, meaning that an update on Atom usually meant an update on Electron, and vice-versa.
Unfortunately, that wasn't the case for a long time...
The latest version of Atom was pulled because of some certificate issues; that means the real latest stable version of Atom was 1.60.0.
This version of Atom used Electron 9.3.5. For comparison, at the time, Electron was at version 22, but the "master" branch of Atom (and atom-community) was at version 11.5.0.
The first thing we did was to find the greatest version that would give us some kind of benefit, but also that we could make work. Luckily for us, the only breaking change on Electron 12 was that some crash analytics changed, and considering we would not use that anymore (because we decided to remove all telemetry) we decided to bump 12.2.3 - the version we're at today.
This might not seem that much of a change, but Electron 12 gave us native macOS ARM binaries, and allowed us to use Playwright to test the editor - enabling what we called "visual tests" on our CI (the actual unit and integration tests from Atom, at the time, were not running reliably in our CI, so these visual tests allowed us to have some feedback if we broke anything).
The next step was to bump to Electron 13. This...
...crashed the editor...
...really bad.
',12),u={href:"https://www.electronjs.org/docs/latest/tutorial/application-debugging",target:"_blank",rel:"noopener noreferrer"},c=t("code",null,"require",-1),m=n('What was crashing was a library called superstring
.
Superstring is a library that handles text manipulation. That basically translates to editing text - so, basically, the most important library that we have.
This library is also a "native library" - meaning that it's written on a lower-level language (C++, in this case) and that it integrates with the Node.JS code via some bindings to the runtime. These bindings basically change all the time on Node.JS, but that's not the worst part.
The worst part is that they change even more agressively on Electron. Because in C, and C++, you manipulate memory manually, and because we're now in a "browser environment" (Electron) some changes are being pushed to avoid corrupting memory, accessing places that we're not supposed to access, and to make these libraries "play nice" in multiple contexts - meaning, one instance of the "browser" not corrupting the memory of another instance (in our case, this translates to one editor window not corrupting another). Superstring was not ready to be used in the state it was, because it manually copied memory from places.
Fortunately, we found a fix - there was an experimental, and incomplete, version of Superstring that compiled to Web Assembly - that means a new format that every browser should run without any problem, and that's how we ported Superstring to run newer Electron.... for a while.
Our syntax highlighter tree-sitter also didn't work on newer Electron version - on Electron 14, it stopped working completely. So, for the time being, we disabled it, and kept trying to port things, fix stuff, etc. Fortunately, all other libraries were ready to be used in newer Electron contexts, and we just had to fix some "synchronous" calls to be "asynchronous" and that worked.
I wasn't that worried about disabling tree-sitter because, at the time, we were already trying to make a modern tree-sitter implementation, so I knew that sometime in the future, we could re-enable tree-sitter and that it would work (spoiler alert: it DID!).
Then I got greedy, and decided to make Pulsar work in the newest Electron at the time - Electron 23.
Well, if you have been following the tone of this post... of course you know what happens next, right?
Electron 21 enabled something called V8 memory cage. I honestly don't fully understand that that means, but what ended up happening is that another native library was crashing the editor - and the name of the library is Oniguruma.
If you're not aware of what this is, it's a regular expression library. If you also don't know what that is, basically is the library used in TextMate grammars to do syntax highlighting.
So, summarizing: Atom (and by definition, Pulsar) had two ways of highlighting code (coloring the keywords, functions, classes, methods, etc): one is called tree-sitter, that breaks in Electron 14, and another callled TextMate, that breaks in Electron 21...
...great.
A code editor without syntax highlighting is probably not a good idea at all; we could not disable it and basically offer no syntax highlighting library. So, I started a quest to migrate TextMate grammars to something different - and what better place to start than VSCode?
',17),p={href:"https://github.com/microsoft/vscode-oniguruma",target:"_blank",rel:"noopener noreferrer"},g={href:"https://github.com/atom/first-mate",target:"_blank",rel:"noopener noreferrer"},w=t("code",null,"second-mate",-1),b=t("code",null,"check-mate",-1),f=t("code",null,"second-mate",-1),y={href:"https://github.com/savetheclocktower",target:"_blank",rel:"noopener noreferrer"},k=t("h2",{id:"the-quest-is-almost-over",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#the-quest-is-almost-over","aria-hidden":"true"},"#"),e(" The quest is (almost) over")],-1),v=t("p",null,[e("I know it seems hard to believe, but after all this work, we were able to bump to Electron 24, then 25, then 26, then 27, and finally, 28 (which "),t("em",null,"did break"),e(" more stuff but luckily that was easy to fix). I'm even writing this post using an experimental build on Electron 28!")],-1),_={href:"https://github.com/bongnv",target:"_blank",rel:"noopener noreferrer"},x=t("p",null,[e("The last piece of code that we needed to fix was the "),t("code",null,"github"),e(" package, but as soon as that was fixed, it was only a matter of completely removing the old implementation of tree-sitter, activating the new one by default, and test, experiment, and do more work.")],-1),q=t("em",null,"a lot",-1),A=t("code",null,"node-pty",-1),E={href:"https://github.com/Spiker985/x-terminal-reloaded/pull/41",target:"_blank",rel:"noopener noreferrer"},I={href:"https://github.com/nteract/hydrogen",target:"_blank",rel:"noopener noreferrer"},T=t("em",null,"very popular package",-1),S=t("p",null,"But for now, we're studying a way to offer the newer Electron branch as an additional download option in our page, so that authors can check if their packages work on this new version. And hopefully, we can get an up-to-date editor, at last!",-1);function M(P,C){const o=l("ExternalLinkIcon");return s(),i("div",null,[d,t("p",null,[e('Trying to open Pulsar in Electron 13 gave us the error "The renderer process crashed". There was no stacktrace, and the '),t("a",u,[e("documentation linked on how to debug this error"),a(o)]),e(" didn't help at all. There were some hidden tutorials to enable CrashReport, then dumping the report somewhere, use a version with debug symbols or download them from GitHub. But honestly, none of this actually pointed to the actual problem, so we did the worst thing that we could think of: we patched "),c,e(" to log what was being required, and hoped for the best.")]),m,t("p",null,[e("It turns out that VSCode uses a WASM version of Oniguruma - that they called "),t("a",p,[e("vscode-oniguruma"),a(o)]),e(" - and it's MIT-licensed too, which is compatible with Pulsar's license. And luckily for us, Pulsar's highlighter (again, inherited from Atom) is on its own separate library, well-tested, called "),t("a",g,[e("first-mate"),a(o)]),e(". So, I did a wordplay and called the new library "),w,e(" (and up to this day, I still reget not calling it "),b,e(" to be honest) that uses this new WASM version. After fixing some infinite loops and other edge-cases that were not fully tested on the original project, I plugged "),f,e(" in the editor, and as unreal that this might sound, it worked!")]),t("p",null,[e("Well, kinda - "),t("a",y,[e("@savetheclocktower"),a(o)]),e(" found that our tests were constantly failing, and found that the reason is that the WASM version doesn't garbage collect the regexes, so it was basically re-creating them over and over again. It is an edge-case that was never going to happen in normal usage, but still it was worth a fix, and then our tests were back to normal... after, of course, we fixed some other issue with macOS on ARM processors.")]),k,v,t("p",null,[e("Also, thanks to the amazing work of "),t("a",_,[e("@bongnv"),a(o)]),e(", we are back to a native version of Superstring (that means we won't take a performance hit from the WASM version - it was, unfortunately, really slow for big files, like the ones I had to work with more than 10,000 lines). This new version still lacks some APIs, but fortunately we don't need them - by patching the specific core packages that used these undocumented, weird APIs we were able to avoid having to implement them.")]),x,t("p",null,[e("There are still things that need to be done - native packages change "),q,e(" in the newer Electron, which translates to some packages simply refusing to load (and the risk of crashing or locking the editor) - for example, no terminal package works, because they all depend on "),A,e(" and that is not supported (luckily, again, for us, "),t("a",E,[e("we have a version of x-terminal-reloaded"),a(o)]),e(" that works); it also breaks "),t("a",I,[e("Hydrogen"),a(o)]),e(", a "),T,e(" that uses ZeroMQ to communicate with Jupyter notebooks (that currently simply doesn't work on the newer Electron branches).")]),S])}const j=r(h,[["render",M],["__file","20240124-mauricioszabo-the-quest-for-electron-lts.html.vue"]]);export{j as default}; diff --git a/assets/20240201-Daeraxa-FebruaryUpdate.html.5a1ade21.js b/assets/20240201-Daeraxa-FebruaryUpdate.html.5a1ade21.js new file mode 100644 index 0000000000..20a6cfbfa5 --- /dev/null +++ b/assets/20240201-Daeraxa-FebruaryUpdate.html.5a1ade21.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-703ed5aa","path":"/blog/20240201-Daeraxa-FebruaryUpdate.html","title":"Community Update","lang":"en-US","frontmatter":{"title":"Community Update","author":"Daeraxa","date":"2024-02-01T00:00:00.000Z","category":["news","log"],"tag":["update"]},"excerpt":"Here it comes sashaying into your feeds, it's the Fab-ruary community update!
\\n","headers":[{"level":2,"title":"Update on the new Tree-sitter implementation","slug":"update-on-the-new-tree-sitter-implementation","link":"#update-on-the-new-tree-sitter-implementation","children":[]},{"level":2,"title":"New blog post about our quest for Electron stable","slug":"new-blog-post-about-our-quest-for-electron-stable","link":"#new-blog-post-about-our-quest-for-electron-stable","children":[]},{"level":2,"title":"New PPR filter options","slug":"new-ppr-filter-options","link":"#new-ppr-filter-options","children":[]},{"level":2,"title":"Major update to our OpenAPI documentation for the PPR","slug":"major-update-to-our-openapi-documentation-for-the-ppr","link":"#major-update-to-our-openapi-documentation-for-the-ppr","children":[]},{"level":2,"title":"Community Spotlight","slug":"community-spotlight","link":"#community-spotlight","children":[]}],"git":{"updatedTime":1706850199000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":3.84,"words":1151},"filePathRelative":"blog/20240201-Daeraxa-FebruaryUpdate.md","localizedDate":"February 1, 2024"}`);export{e as data}; diff --git a/assets/20240201-Daeraxa-FebruaryUpdate.html.f8bbd93f.js b/assets/20240201-Daeraxa-FebruaryUpdate.html.f8bbd93f.js new file mode 100644 index 0000000000..b055dc6850 --- /dev/null +++ b/assets/20240201-Daeraxa-FebruaryUpdate.html.f8bbd93f.js @@ -0,0 +1 @@ +import{_ as s}from"./tree-sitter.6a39e323.js";import{_ as n}from"./electron.b5867f08.js";import{_ as i}from"./package.4e7449ae.js";import{_ as l}from"./spotlight.aba19e76.js";import{_ as h,o as d,c as u,e as c,a as e,b as t,d as a,f as r,r as p}from"./app.0e1565ce.js";const f="/assets/openapi-logo.f023afce.png",m={},g=e("p",null,[t("Here it comes sashaying into your feeds, it's the "),e("em",null,"Fab"),t("-ruary community update!")],-1),_=e("h1",{id:"welcome-to-the-february-community-update",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#welcome-to-the-february-community-update","aria-hidden":"true"},"#"),t(" Welcome to the February Community Update!")],-1),b={href:"https://github.com/mauricioszabo",target:"_blank",rel:"noopener noreferrer"},w=r('With the release of Pulsar 1.113 we finally went live with our new Tree-sitter implementation, which had been in the works for a long time. We had tested this as much as we were able to with the resources we had, and we made sure that any issues raised by people enabling the new feature were dealt with. However, with it being enabled by default, we suddenly had significantly more people using this feature, and thus issues we hadn't yet seen were being found.
',3),y={href:"https://github.com/savetheclocktower",target:"_blank",rel:"noopener noreferrer"},k={href:"https://github.com/pulsar-edit/pulsar/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+MEGA-ISSUE+label%3Abug",target:"_blank",rel:"noopener noreferrer"},v={href:"https://pulsar-edit.dev/download.html#rolling-release",target:"_blank",rel:"noopener noreferrer"},x={href:"https://github.com/pulsar-edit/pulsar/issues/875",target:"_blank",rel:"noopener noreferrer"},P={href:"https://pulsar-edit.dev/community.html",target:"_blank",rel:"noopener noreferrer"},T=r('Currently, these filters are only available via the API directly, so they do not feature within Pulsar itself or in the PPR website UI, but we are looking to add this functionality in the future to make finding the exact package you want much easier.
Now if only there was a way to find what all these different endpoints might be and how you could construct even fancier queries... well, if only there was a...
Pulsar 1.114.0
Back in Pulsar 1.111.0, we created a new UI
api to make an easy and universal way for packages to interact with Pulsar. This implementation did contain a couple of small oversights, which have now been resolved. One issue with the markdown implementation was the display of non-functional heading anchors when rendering a package README; the other was a small rendering bug that was causing a misalignment of content after a linebreak. We had also inadvertently introduced a new problem with fuzzymatcher
that could cause the command palette to hang if it failed to highlight a match.
One issue raised a while ago was that, starting with Pulsar 1.109.0, users of older LTS Linux distributions, such as AlmaLinux 8 and RockyLinux 9, were no longer able to run Pulsar. This was due to the CI runners we use being updated to newer OSs, and therefore Pulsar was being compiled against a newer version of glibc
. We have changed our workflows to compile Pulsar in a Debian 10 Docker container instead, which should resolve this problem.
It was noticed a while ago that the github
package wasn't quite working correctly on ARM Linux systems. We have bumped the version of dugite
(a library for using git
in NodeJS and Electron) to resolve the problem.
Lastly, we have two new features in ppm
. The first is a new flag --force
that can be specified on ppm link
, which will forcibly replace a conflicting package without needing to manually remove it. The other is an update to ppm
's syntax theme template so that, when generating a new syntax theme, it now includes some style variables that the editor has long supported, but which were not in the template, so theme authors might have been unaware of them. (As a bonus: there are two new variables supported by default in the editor, and added to the template as well! Theme authors, feel free to take advantage of these!)
And that is it for yet another Pulsar release. We hope you enjoy this release and look forward to the next one!
Until next time, happy coding, and see you amongst the stars!
settings-view
.atom.ui.markdown.render
when disableMode
was set to "strict"
and the input contained HTML line breaks.--force
flag to ppm link
command that will uninstall any conflicting package already installed.syntax-variables.less
.Last month\u2019s 1.114.0 release was full of fixes related to the recent migration to modern Tree-sitter. This month\u2019s release is much smaller, but still dominated by Tree-sitter fixes affecting syntax highlighting, code folding, and indentation.
The most visible fixes are related to your ability to customize the grammar that Pulsar uses on a per-language basis. This is an approach we\u2019ve encouraged for users that want or need to revert to an older grammar for a specific language \u2014 better to do so on a targeted basis than globally. If you can edit your config file, you can do per-language customization.
For instance, now it\u2019s even easier than before to say \u201Cuse legacy Tree-sitter, but only for Python\u201D:
".python.source":
+ core:
+ useLegacyTreeSitter: true
+
Or \u201Cuse modern Tree-sitter for JavaScript, but TextMate-style grammars everywhere else\u201D:
"*":
+ core:
+ useTreeSitterParsers: false
+".source.javascript":
+ core:
+ useTreeSitterParsers: true
+
Better yet, now the grammar-selector
package will be attuned to these choices. When you manually reassign a buffer to use a different grammar, it will offer you only the \u201Ccorrect\u201D grammar for each language based on what you\u2019ve opted into.
We\u2019ve also delivered our customary incremental improvements in language support, and one change that affects nearly all languages: indentation hinting will be more accurate in transactions with multiple buffer changes. The most common example of a multi-edit transaction is when the user places more than one cursor and starts typing.
We\u2019ve made improvements to our language-ruby
bundle, primarily with code folding and indentation hinting. TypeScript and C/C++ also got some small enhancements, and the language-shellscript
bundle got a parser update.
And lastly, on another note, we have a few maintenance and upkeep PR's to keep our Cirrus CI active and working, to ensure you can keep using and enjoying the latest builds Pulsar has to offer.
Until next time, happy coding, and see you amongst the stars!
unless
, some blocks, multiline comments, function calls, and different array syntaxes for strings and keywords.grammar-selector
package so that it will show the user's preferred grammar for a specific language.0.20.9
of web-tree-sitter
.Pulsar 1.115.0
Like the past few releases, this Pulsar release is full of Tree-sitter improvements and fixes! From TypeScript and Python getting improved syntax highlighting all around, to big changes like replacing the underlying Markdown Tree-sitter parser, Pulsar 1.116 is sure to make these grammars more stable and aesthetically pleasing.
But on to some of the really exciting stuff this month: we have a flurry of new features and updates to snippets
, which is sure to make otherwise repetitive coding exciting and high-tech!
For anyone familiar with the super secret code ninja techniques of snippets
, there's now support for new snippet variables and transformation flags. Be sure to read the full changelog to get caught up on all of these changes.
As always, this release has some bug fixes and housekeeping. Linux folks will appreciate improved metadata for their installs, whereas Windows folks will be happy to see that adding Pulsar to the PATH is easier than ever whether Pulsar is installed for just one user or system-wide.
That's just about everything this time around. We can't wait to see what people do with these powerful new snippets
features and look forward to the next one!
Until next time, happy coding, and see you amongst the stars! - The Pulsar team
TextEditor::getCommentDelimitersForBufferPosition
for retrieving comment delimiter strings appropriate for a given buffer position. This allows us to support three new snippet variables: LINE_COMMENT
, BLOCK_COMMENT_START
, and BLOCK_COMMENT_END
./upcase
and /camelcase
) within sed
-style snippet transformation replacements.Pulsar 1.116.0
Pulsar 1.117.0
As you may have come to expect, this release is full of new improvements for Tree-sitter grammars! From SCSS now having Tree-sitter support, to Ruby getting an updated parser, to performance and highlighting enhancements in Markdown and TypeScript, there's hopefully something in there for everybody.
Speaking of Markdown, the markdown-preview
package has gotten significant performance increases in the preview pane (especially for documents with lots of fenced code blocks), and boasts brand new dark mode support for the "GitHub.com style" option!
But dark mode support doesn't stop there! Pulsar can now (optionally) use the OS-level dark mode window theme, switching automatically based on the lightness or darkness of the colors in the active Pulsar theme if enabled.
Beyond the fixes and features within the editor itself, you'll be glad to know this release also saw many improvements for our Continuous Integration to ensure we can deliver timely and bug-free updates in the future. - The Pulsar team
core.syncWindowThemeWithPulsarTheme
is enabled.Get your grills ready, Pulsar v1.118.0 is cooking with gas! This release brings lots of love to syntax highlighting, along with a zesty sprinkling of features and fixes.
We've got Tree-sitter fixes and improvements, including improved documentation around Tree-sitter's usage, an updated PHP parser, fixed syntax quoting on Clojure, as well as enhanced Clojure highlighting support for metadata and "def" elements, as well as improved injection points for Clojure. EDN files are back to being detected as Clojure, but will highlight correctly as EDN. Tasty!
Within the Pulsar application the new setting core.allowWindowTransparency
lets users set the background of the editor to support transparency, although transparent CSS styles must be set by a theme or user stylesheet for Pulsar to actually be transparent. The textChanged
property is now accurate when characters are deleted.
The Tree-sitter syntax highlighting system now has a new test for queries ancestorTypeNearerThan
which helps matching the first type as an ancestor, as well as supporting a second buffer
argument in the content
field of addInjectionPoint
for easier customization.
Finally, for our community package developers, they will be glad to hear that first time publication issues have been resolved with a rewrite of the ppm publish
command, improving the workflow, and ensuring things work properly with the updated backend. Please feel free to share what you've been cooking by publishing your packages!
As always, thanks a ton to all of our users and supporters for sticking with the Pulsar project, and until next time, happy coding, and see you amongst the stars!
- The Pulsar Team
core.allowWindowTransparency
so that themes and user stylesheets can make editor windows' backgrounds transparent.ancestorTypeNearerThan
that matches if it finds the first type as an ancestor, but doesn't match if any "other" ancestors are found beforecontent
field of addInjectionPoint for modern-tree-sitter now supports a second buffer
argument, for better customization if one wants todefault
or definition
as a def
, but highlights p/defresolver
)textChanged
property to be accurate when deleting charactersppm publish
for publishing brand new packagesPulsar 1.118.0
Pulsar 1.119.0
While a smaller release this time around, v1.119.0 still manages to pack a punch.
For macOS, we've gone to great lengths to ensure Pulsar should build just fine on macOS 13+, while our Linux users get greater compatibility for DevTools on various platforms. For our programmers, there's been more of the constant incremental improvements to various languages' built-in syntax highlighting and code folding this time around, with a focus on PHP, Python, Javascript, Typescript, Shell script, and C.
As always thanks a ton to all of those that support the project and keep it moving forward, we appreciate you all, and look forward to seeing you amongst the stars.
- The Pulsar Team
Notice
Originally, the binaries from our normal "Pull Request" CI build (which produces unsigned binaries) were accidentally uploaded to this release, instead of the binaries from the special "tag push" CI build (which signs the macOS binaries). In order to provide you with signed Intel macOS binaries, we have re-uploaded them and updated SHA256SUMS.txt
to reflect this. The following binaries have been swapped out for the correct versions and the checksums updated:
Intel.Mac.Pulsar-1.119.0-mac.zip
Intel.Mac.Pulsar-1.119.0.dmg
language-php
to continue syntax-highlighting even when encountering unbalanced PHP tags. (Avoid throwing a syntax error)language-python
, language-javascript
, language-typescript
, language-shell
and language-c
.libiconv
vs GNU libiconv
) in the iconv
library shipped in macOS 13+--no-sandbox
flag not being applied to the .desktop
launcher on Linux (Fixes Dev Tools)Pulsar
\\nTeam
\\nResources
\\ntitle: Example Post +author: Daeraxa +date: 2022-11-12 +category:
+This is the excerpt.
+`},["/blog/20221112-Daeraxa-ExamplePost","/blog/20221112-Daeraxa-ExamplePost.md"]],["v-0075970e","/blog/20221127-confused-Techie-SunsetMisadventureBackend.html",{title:"A Sunset and the Misadventures of a Backend",type:"article",readingTime:{minutes:6.49,words:1948},excerpt:`What's the natural response to finding out an application you use is being sunset? +Spend the next several months creating a complex backend infrastructure of course!
+`,author:"confused-Techie",date:"2022-11-27T00:00:00.000Z",localizedDate:"November 27, 2022",category:["dev","log"],tag:["backend","sunset"]},["/blog/20221127-confused-Techie-SunsetMisadventureBackend","/blog/20221127-confused-Techie-SunsetMisadventureBackend.md"]],["v-cfc0df38","/blog/20221208-Daeraxa-DistroTubeVideo.html",{title:"Featured on DistroTube!",type:"article",readingTime:{minutes:.51,words:154},excerpt:`Today we were featured on the DistroTube YouTube channel!
+Watch here
Check out our first GitHub Release for Pulsar! Available Now!
Check out our Second GitHub Release for Pulsar! Available Now!
What has the Pulsar team and community been up to lately?
+`,author:"Daeraxa",date:"2023-02-01T00:00:00.000Z",localizedDate:"February 1, 2023",category:["news","log"],tag:["update"]},["/blog/20230201-Daeraxa-FebUpdate","/blog/20230201-Daeraxa-FebUpdate.md"]],["v-5fdcc4a1","/blog/20230209-mauricioszabo-tree-sitter-part-1.html",{title:"Tales of Tree-Sitter part 1: the start of a tale",type:"article",readingTime:{minutes:2.96,words:889},excerpt:`How did I decide to start working on tree-sitter, and all preparations to +modernize it on Pulsar
+`,author:"Maur\xEDcio",date:"2023-02-09T00:00:00.000Z",localizedDate:"February 9, 2023",category:["dev"],tag:["modernization","tree-sitter"]},["/blog/20230209-mauricioszabo-tree-sitter-part-1","/blog/20230209-mauricioszabo-tree-sitter-part-1.md"]],["v-f88b1c54","/blog/20230215-Daeraxa-v1.102.0.html",{title:"New Regular Release (v1.102.0)",type:"article",readingTime:{minutes:2.89,words:867},excerpt:`Check out our newest Regular Release for Pulsar! Available Now!
We recently decided to change our release strategy to reflect what we are actually doing and address some potential inaccuracies in our existing terminology.
+`,author:"Daeraxa",date:"2023-02-16T00:00:00.000Z",localizedDate:"February 16, 2023",category:["news"],tag:["releases","rolling","regular"]},["/blog/20230216-Daeraxa-ReleaseStrategyUpdate","/blog/20230216-Daeraxa-ReleaseStrategyUpdate.md"]],["v-5b193c62","/blog/20230227-Daeraxa-Survey1.html",{title:"Survey: How did you hear about us?",type:"article",readingTime:{minutes:.42,words:125},excerpt:`A short survey asking how you heard about the Pulsar project as well as feedback +on our community and social presence.
+`,author:"Daeraxa",date:"2023-02-27T00:00:00.000Z",localizedDate:"February 27, 2023",category:["survey"],tag:["community","socials"]},["/blog/20230227-Daeraxa-Survey1","/blog/20230227-Daeraxa-Survey1.md"]],["v-29fd1295","/blog/20230301-Daeraxa-MarUpdate.html",{title:"Community Update",type:"article",readingTime:{minutes:2.7,words:810},excerpt:`What has the Pulsar team and community been up to lately? Find out here!
+`,author:"Daeraxa",date:"2023-03-01T00:00:00.000Z",localizedDate:"March 1, 2023",category:["news","log"],tag:["update"]},["/blog/20230301-Daeraxa-MarUpdate","/blog/20230301-Daeraxa-MarUpdate.md"]],["v-6fcbebb6","/blog/20230315-Daeraxa-v1.103.0.html",{title:"It's that time again, Pulsar 1.103.0 is available now!",type:"article",readingTime:{minutes:2.25,words:676},excerpt:`Check out our newest Regular Release for Pulsar! Available Now!
How setting license: 'none'
removed almost 100 Packages from the Pulsar Package Registry.
Here we have the results and analysis of our first community survey (and an infographic for those who like that kind of thing).
+`,author:"Daeraxa",date:"2023-03-26T00:00:00.000Z",localizedDate:"March 26, 2023",category:["survey"],tag:["community","socials"]},["/blog/20230326-Daeraxa-Survey1-Results","/blog/20230326-Daeraxa-Survey1-Results.md"]],["v-e1c35862","/blog/20230401-Daeraxa-AprUpdate.html",{title:"Community Update",type:"article",readingTime:{minutes:5.91,words:1774},excerpt:`Another dose of our regular monthly community update!
+`,author:"Daeraxa",date:"2023-04-01T00:00:00.000Z",localizedDate:"April 1, 2023",category:["news","log"],tag:["update"]},["/blog/20230401-Daeraxa-AprUpdate","/blog/20230401-Daeraxa-AprUpdate.md"]],["v-0af04e6e","/blog/20230401-confused-Techie-PON.html",{title:"Meet PON (Pulsar Object Notation)",type:"article",readingTime:{minutes:1.75,words:525},excerpt:`The release of PON!
+`,author:"confused-Techie",date:"2023-04-01T00:00:00.000Z",localizedDate:"April 1, 2023",category:["dev"],tag:["joke"]},["/blog/20230401-confused-Techie-PON","/blog/20230401-confused-Techie-PON.md"]],["v-3a723159","/blog/20230418-Daeraxa-v1.104.0.html",{title:"Get Ready for another fantastically essential release, Pulsar 1.104.0 is now available!",type:"article",readingTime:{minutes:2.51,words:753},excerpt:`Check out our newest Regular Release for Pulsar! Available Now!
A short survey asking for community feedback on a couple of aspects of the project
+`,author:"Daeraxa",date:"2023-04-30T00:00:00.000Z",localizedDate:"April 30, 2023",category:["survey"],tag:["community","socials"]},["/blog/20230430-Daeraxa-Survey2","/blog/20230430-Daeraxa-Survey2.md"]],["v-638b3e6c","/blog/20230501-Daeraxa-MayUpdate.html",{title:"Community Update",type:"article",readingTime:{minutes:6.49,words:1947},excerpt:`Is it a bird? Is it a plane? No it's the May community update!
+`,author:"Daeraxa",date:"2023-05-01T00:00:00.000Z",localizedDate:"May 1, 2023",category:["news","log"],tag:["update"]},["/blog/20230501-Daeraxa-MayUpdate","/blog/20230501-Daeraxa-MayUpdate.md"]],["v-dbbf0e92","/blog/20230516-Daeraxa-v1.105.0.html",{title:"Welcome to a rad new release, Pulsar 1.105.0 is available now!",type:"article",readingTime:{minutes:3.13,words:939},excerpt:`Welcome to a rad new release, Pulsar 1.105.0 is available now!
Here we have the results and comments of our second community survey!
+`,author:"Daeraxa",date:"2023-05-25T00:00:00.000Z",localizedDate:"May 25, 2023",category:["survey"],tag:["community","feedback"]},["/blog/20230525-Daeraxa-Survey2-Results","/blog/20230525-Daeraxa-Survey2-Results.md"]],["v-54f2f18e","/blog/20230601-Daeraxa-JuneUpdate.html",{title:"Community Update",type:"article",readingTime:{minutes:7.95,words:2385},excerpt:`What's this? No... it can't be. But it is! It's the newest instalment of the Pulsar Community Update!
+`,author:"Daeraxa",date:"2023-06-01T00:00:00.000Z",localizedDate:"June 1, 2023",category:["news","log"],tag:["update"]},["/blog/20230601-Daeraxa-JuneUpdate","/blog/20230601-Daeraxa-JuneUpdate.md"]],["v-25d26d73","/blog/20230610-Daeraxa-2kStars.html",{title:"2k Stars!",type:"article",readingTime:{minutes:.54,words:161},excerpt:`We just hit 2000 stars on GitHub!
+`,author:"Daeraxa",date:"2023-06-10T00:00:00.000Z",localizedDate:"June 10, 2023",category:["news"],tag:["github","stars"]},["/blog/20230610-Daeraxa-2kStars","/blog/20230610-Daeraxa-2kStars.md"]],["v-7e31f297","/blog/20230616-Daeraxa-v1.106.0.html",{title:"Pulsar v1.106.0: A Focus on Grammars",type:"article",readingTime:{minutes:3.65,words:1096},excerpt:`Welcome to our latest release, Pulsar 1.106.0 is available now!
What has it got in its pocketses? It's the July community update!
+`,author:"Daeraxa",date:"2023-07-01T00:00:00.000Z",localizedDate:"July 1, 2023",category:["news","log"],tag:["update"]},["/blog/20230701-Daeraxa-JulyUpdate","/blog/20230701-Daeraxa-JulyUpdate.md"]],["v-5e247e06","/blog/20230715-DeeDeeG-v1.107.0.html",{title:"Fresh off the CI press: Pulsar 1.107.0 is available now!",type:"article",readingTime:{minutes:3.97,words:1190},excerpt:`Fresh off the CI press: Pulsar 1.107.0 is available now!
Hotfix: Pulsar 1.107.1 is available now!
How many roads must a man walk down? Not a clue, but what I do know is that this is the one and only Pulsar Community Update!
+`,author:"Daeraxa",date:"2023-08-01T00:00:00.000Z",localizedDate:"August 1, 2023",category:["news","log"],tag:["update"]},["/blog/20230801-Daeraxa-AugustUpdate","/blog/20230801-Daeraxa-AugustUpdate.md"]],["v-0730d41e","/blog/20230816-DeeDeeG-v1.108.0.html",{title:"Great Scott! A new Pulsar release. Pulsar 1.108.0 is available now!",type:"article",readingTime:{minutes:3.8,words:1140},excerpt:`Great Scott! A new Pulsar release. Pulsar 1.108.0 is available now!
An update on the status of our Chocolatey packages.
+`,author:"Daeraxa",date:"2023-08-25T00:00:00.000Z",localizedDate:"August 25, 2023",category:["news"],tag:["windows","chocolatey","package manager"]},["/blog/20230825-Daeraxa-ChocolateyUpdate","/blog/20230825-Daeraxa-ChocolateyUpdate.md"]],["v-356c6c42","/blog/20230903-confused-Techie-pulsars-ci.html",{title:"Pulsar's CI",type:"article",readingTime:{minutes:6.43,words:1930},excerpt:`How Pulsar's CI has recently changed.
+`,author:"confused-Techie",date:"2023-09-07T00:00:00.000Z",localizedDate:"September 7, 2023",category:["dev"],tag:["ci"]},["/blog/20230903-confused-Techie-pulsars-ci","/blog/20230903-confused-Techie-pulsars-ci.md"]],["v-4dac8919","/blog/20230904-Daeraxa-SeptemberUpdate.html",{title:"Community Update",type:"article",readingTime:{minutes:4.34,words:1303},excerpt:`I want you to act as a Pulsar team member writing a blog post. I will provide titles of recent changes. You will elaborate on each topic and make each paragraph coherent from my ramblings. You will welcome all readers to the September Pulsar community update.
+`,author:"Daeraxa",date:"2023-09-04T00:00:00.000Z",localizedDate:"September 4, 2023",category:["news","log"],tag:["update"]},["/blog/20230904-Daeraxa-SeptemberUpdate","/blog/20230904-Daeraxa-SeptemberUpdate.md"]],["v-42666037","/blog/20230916-Daeraxa-v1.109.0.html",{title:"Going the whole nine yards: Get Pulsar 1.109.0 now!",type:"article",readingTime:{minutes:5.33,words:1598},excerpt:`Going the whole nine yards: Get Pulsar 1.109.0
Join !pulsaredit@lemmy.ml!
+`,author:"Daeraxa",date:"2023-09-17T00:00:00.000Z",localizedDate:"September 17, 2023",category:["news"],tag:["windows","chocolatey","package manager"]},["/blog/20230917-Daeraxa-LemmyCommunity","/blog/20230917-Daeraxa-LemmyCommunity.md"]],["v-25bd77f6","/blog/20230925-savetheclocktower-modern-tree-sitter-part-1.html",{title:"Modern Tree-sitter, part 1: the new old feature",type:"article",readingTime:{minutes:8.62,words:2585},excerpt:`The last few releases of Pulsar have been bragging about a feature that arguably isn\u2019t even new: our experimental \u201Cmodern\u201D Tree-sitter implementation. You might\u2019ve read that phrase a few times now without fully understanding what it means, and an explanation is long overdue.
+`,author:"savetheclocktower",date:"2023-09-25T00:00:00.000Z",localizedDate:"September 25, 2023",category:["dev"],tag:["modernization","tree-sitter"]},["/blog/20230925-savetheclocktower-modern-tree-sitter-part-1","/blog/20230925-savetheclocktower-modern-tree-sitter-part-1.md"]],["v-5bad2e3c","/blog/20230927-savetheclocktower-modern-tree-sitter-part-2.html",{title:"Modern Tree-sitter, part 2: why scopes matter",type:"article",readingTime:{minutes:7.84,words:2351},excerpt:`In
Today I\u2019ll try to illustrate what that system looks like and why it\u2019s important.
+`,author:"savetheclocktower",date:"2023-09-27T00:00:00.000Z",localizedDate:"September 27, 2023",category:["dev"],tag:["modernization","tree-sitter"]},["/blog/20230927-savetheclocktower-modern-tree-sitter-part-2","/blog/20230927-savetheclocktower-modern-tree-sitter-part-2.md"]],["v-6cc4d458","/blog/20231004-Daeraxa-OctoberUpdate.html",{title:"Community Update",type:"article",readingTime:{minutes:5.58,words:1674},excerpt:`As the leaves turn brown and the days grow shorter, make sure you draw up a chair and settle in for a nice, warm, pumpkin spice edition of the Pulsar community update!
+`,author:"Daeraxa",date:"2023-10-04T00:00:00.000Z",localizedDate:"October 4, 2023",category:["news","log"],tag:["update"]},["/blog/20231004-Daeraxa-OctoberUpdate","/blog/20231004-Daeraxa-OctoberUpdate.md"]],["v-8b6c6124","/blog/20231013-savetheclocktower-modern-tree-sitter-part-3.html",{title:"Modern Tree-sitter, part 3: syntax highlighting via queries",type:"article",readingTime:{minutes:15.79,words:4738},excerpt:`Last time I laid out the case
Today I\u2019d like to show you the specific problems that we had to solve in order to pull that off.
+`,author:"savetheclocktower",date:"2023-10-13T00:00:00.000Z",localizedDate:"October 13, 2023",category:["dev"],tag:["modernization","tree-sitter"]},["/blog/20231013-savetheclocktower-modern-tree-sitter-part-3","/blog/20231013-savetheclocktower-modern-tree-sitter-part-3.md"]],["v-e8324d12","/blog/20231016-Daeraxa-v1.110.0.html",{title:"Armed with a big ol' can of Raid: Pulsar 1.110.0 is available now!",type:"article",readingTime:{minutes:3.97,words:1192},excerpt:`Armed with a big ol' can of Raid: Pulsar 1.110.0
Last time we looked at Tree-sitter\u2019s query system and showed how it can be used
Today we\u2019ll look at two other systems \u2014 indentation hinting and code folding \u2014 and I\u2019ll explain how queries can be used to support each one.
+`,author:"savetheclocktower",date:"2023-10-31T00:00:00.000Z",localizedDate:"October 31, 2023",category:["dev"],tag:["modernization","tree-sitter"]},["/blog/20231031-savetheclocktower-modern-tree-sitter-part-4","/blog/20231031-savetheclocktower-modern-tree-sitter-part-4.md"]],["v-879bed80","/blog/20231109-Daeraxa-NovemberUpdate.html",{title:"Community Update",type:"article",readingTime:{minutes:3.85,words:1156},excerpt:`What month is most important for a Pulsar? A supernova-ember! ...What isn't a bad joke is this, the Pulsar community update!
+`,author:"Daeraxa",date:"2023-11-09T00:00:00.000Z",localizedDate:"November 9, 2023",category:["news","log"],tag:["update"]},["/blog/20231109-Daeraxa-NovemberUpdate","/blog/20231109-Daeraxa-NovemberUpdate.md"]],["v-168f28ae","/blog/20231110-savetheclocktower-modern-tree-sitter-part-5.html",{title:"Modern Tree-sitter, part 5: injections",type:"article",readingTime:{minutes:16.04,words:4812},excerpt:`One annoying thing that software developers do is insist on writing in more than one language at once. Web developers are espeically obnoxious about this \u2014 routinely, for instance, putting CSS inside their HTML, or HTML inside their JavaScript, or CSS inside their HTML inside their JavaScript.
+Code editors like Pulsar need to roll with this, so today we\u2019ll talk about how the modern Tree-sitter system handles what we call injections.
+`,author:"savetheclocktower",date:"2023-11-10T00:00:00.000Z",localizedDate:"November 10, 2023",category:["dev"],tag:["modernization","tree-sitter"]},["/blog/20231110-savetheclocktower-modern-tree-sitter-part-5","/blog/20231110-savetheclocktower-modern-tree-sitter-part-5.md"]],["v-77f85357","/blog/20231116-Daeraxa-v1.111.0.html",{title:"If you're API and you know it, clap your hands!",type:"article",readingTime:{minutes:3.71,words:1113},excerpt:`Pulsar 1.111.0
If you have been good this year, you might just find something in your stocking this Christmas. Until then, you can find the Pulsar community update right here!
+`,author:"Daeraxa",date:"2023-12-11T00:00:00.000Z",localizedDate:"December 11, 2023",category:["news","log"],tag:["update"]},["/blog/20231212-Daeraxa-DecemberUpdate","/blog/20231212-Daeraxa-DecemberUpdate.md"]],["v-6409cd37","/blog/20231216-Daeraxa-v1.112.0.html",{title:"Christmas has come early",type:"article",readingTime:{minutes:3.57,words:1071},excerpt:`Pulsar 1.112.0
Hotfix: Pulsar 1.112.1 is available now!
Happy new year! Welcome to the first Pulsar community update of 2024!
+`,author:"Daeraxa",date:"2024-01-12T00:00:00.000Z",localizedDate:"January 12, 2024",category:["news","log"],tag:["update"]},["/blog/20240112-Daeraxa-JanuaryUpdate","/blog/20240112-Daeraxa-JanuaryUpdate.md"]],["v-3500d176","/blog/20240115-Daeraxa-v1.113.0.html",{title:"Unlucky for some, but not us. Our 13th release, Pulsar 1.113.0, is available now!",type:"article",readingTime:{minutes:3.46,words:1039},excerpt:`Pulsar 1.113.0
We\u2019ve been telling a series of stories about all the different ways that Tree-sitter can improve the editing experience in Pulsar. Today\u2019s story about symbols-view
starts a bit slowly, but it\u2019s got a great ending:\xA0the addition of a major new feature to Pulsar 1.113.
Here it comes sashaying into your feeds, it's the Fab-ruary community update!
+`,author:"Daeraxa",date:"2024-02-01T00:00:00.000Z",localizedDate:"February 1, 2024",category:["news","log"],tag:["update"]},["/blog/20240201-Daeraxa-FebruaryUpdate","/blog/20240201-Daeraxa-FebruaryUpdate.md"]],["v-21124b56","/blog/20240215-Daeraxa-v1.114.0.html",{title:"A Valentine's release bursting with love, Pulsar 1.114.0 is available now!",type:"article",readingTime:{minutes:3.7,words:1110},excerpt:`Pulsar 1.114.0
Pulsar 1.115.0
Pulsar 1.116.0
Pulsar 1.117.0
Pulsar 1.118.0
Pulsar 1.119.0
This is a guide on how to add a blog post to the website which will be shown on
+https://pulsar-edit.dev/article/
We are using the Vuepress Blog Plugin
This is all implemented in the main website repository
YYYYMMDD-<author>-<title>.md
e.g 20221031-CreativeUsername-ThisIsMyBlogPost.md
title
, author
, date
, category
+and tag
as the minimum as the others will default to false.
+title
- String: The displayed title of the post, consider this as H1
author
- String: The name of the author to be displayed.date
- Date (ISO 8601): Allows display of date as well as enabling
+sorting on the timeline, set to the same as your filename date but with
+hyphens (e.g. 2022-10-31
).category
- String (multiline): Enables filtering by category, this should be based on the
+subject of the post e.g. release
, dev log
, announcement
. This is a
+multiline field if you want to set more than one category.tag
- String (multiline): Enables filtering by tags, this should be based on the content of
+the post and areas it touches on e.g. website
, editor
, config
.sticky
- Bool: Enables "pinning" on thestar
- Bool: Enables use of the star
category for any important articles
+we want to remain visible.article
- Bool: You probably won't want to use this but setting this to false
+will exclude this page from appearing. This is set on the "example" blog
+post intentionally.Now that Atom is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Atom for the first time, you should get a screen that looks like this:
This is the Atom welcome screen and gives you a pretty good starting point for how to get started with the editor.
In that welcome screen, we are introduced to probably the most important command in Atom, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like Cmd+Shift+PCtrl+Shift+P to demonstrate how to run a command. These are the default keybindings for the platform that we detected you running.
If you want to see a different platform than the one we detected, you may choose a different one by using the platform selector near the top of the page:
If the Platform Selector is not present, then the current page doesn't have any platform-specific content.
If you have customized your Atom keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Atom. Instead of clicking around all the application menus to look for something, you can press Cmd+Shift+PCtrl+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut keystrokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Atom has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Atom Packages.
To open the Settings View, you can: ::: tabs Settings View
:::
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Atom. Atom ships with 4 different UI themes, dark and light variants of the Atom and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
There are also dozens of themes on https://atom.io that you can choose from if you want something different. We will cover customizing a theme in Style Tweaks and creating your own theme in Creating a Theme.
You can use the Settings View to specify your whitespace and wrapping preferences.
Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the Tab key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.
The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.
',13),S=a('Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Atom. You can do it by choosing File > Open from the menu bar or by pressing Cmd+O Ctrl+O to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
',6),C=t("code",null,"atom",-1),T={class:"platform-mac"},A=t("code",null,"atom",-1),P=t("code",null,"apm",-1),F={class:"platform-windows platform-linux"},I=t("code",null,"atom",-1),V=t("code",null,"apm",-1),q=a(`You can run the atom
command with one or more file paths to open up those files in Atom.
$ atom --help
+> Atom Editor v1.8.0
+
+> Usage: atom [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Atom window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off atom [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+content/getting-started/sections/atom-basics.md:84:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ atom content/getting-started/sections/atom-basics.md:84
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+content/getting-started/sections/atom-basics.md:150:722
+$ atom content/getting-started/sections/atom-basics.md:150:722
+
To save a file you can choose File > Save from the menu bar or Cmd+SCtrl+S to save the file. If you choose File > Save As or press Cmd+Shift+SCtrl+Shift+S then you can save the current content in your editor under a different file name. Finally, you can choose File > Save All or press Alt+Cmd+S to save all the open files in Atom.
Atom doesn't just work with single files though; you will most likely spend most of your time working on projects with multiple files. To open a directory, choose the menu item File > OpenFile > Open Folder and select a directory from the dialog. You can also add more than one directory to your current Atom window, by choosing File > Add Project Folder from the menu bar or pressing Cmd+Shift+OCtrl+Shift+A.
You can open any number of directories from the command line by passing their paths to the atom
command line tool. For example, you could run the command atom ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Atom with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
You can also hide and show it with Cmd+\\Ctrl+\\ or the tree-view: toggle
command from the Command Palette, and Ctrl+0 Alt+\\ will focus it. When the Tree view has focus you can press A, M, or Delete to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in FinderWindows Exploreryour native filesystem or copying the file path to the clipboard.
Note
Atom Packages
Like many parts of Atom, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Atom by default. Packages that are bundled with Atom are referred to as Core packages. Ones that aren't bundled with Atom are referred to as Community packages.
You can find the source code to the Tree View on GitHub at https://github.com/atom/tree-view.
This is one of the interesting things about Atom. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.
Once you have a project open in Atom, you can easily find and open any file within that project.
If you press Cmd+TCtrl+T or Cmd+PCtrl+P, the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.
You can also search through only the files currently opened (rather than every file in your project) with Cmd+BCtrl+B. This searches through your "buffers" or open files. You can also limit this fuzzy search with Cmd+Shift+BCtrl+Shift+B, which searches only through the files which are new or have been modified since your last Git commit.
',14),j=t("code",null,"core.ignoredNames",-1),N=t("code",null,"fuzzy-finder.ignoredNames",-1),Y=t("code",null,"core.excludeVCSIgnoredPaths",-1),B={href:"https://git-scm.com/docs/gitignore",target:"_blank",rel:"noopener noreferrer"},W=t("code",null,".gitignore",-1),E=t("code",null,"core.ignoredNames",-1),L=t("code",null,"fuzzy-finder.ignoredNames",-1),U={href:"https://github.com/isaacs/minimatch",target:"_blank",rel:"noopener noreferrer"},$=a('Tips
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
All requests that take parameters require application/json
.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to downloads
asc
or desc
. Defaults to desc
. stars
can only be ordered desc
Returns a list of all packages in the following format:
[
+ {
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ }
+ },
+ ...
+ ]
+
Results are paginated 30 at a time, and links to the next and last pages are provided in the Link
header:
Link: <https://www.atom.io/api/packages?page=1>; rel="self",
+ <https://www.atom.io/api/packages?page=41>; rel="last",
+ <https://www.atom.io/api/packages?page=2>; rel="next"
+
By default, results are sorted by download count, descending.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to the relevance of the search query.asc
or desc
. Defaults to desc
.Returns results in the same format as listing packages.
Returns package details and versions for a single package
Parameters:
`,22),x=a("strong",null,"engine",-1),w={href:"http://semver.org",target:"_blank",rel:"noopener noreferrer"},j=t(`Returns:
{
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ },
+ "versions": [
+ (see single version output below)
+ ...,
+ ]
+ }
+
Create a new package; requires authentication.
The name and version will be fetched from the package.json
file in the specified repository. The authenticating user must have access to the indicated repository.
Parameters:
Returns:
Delete a package; requires authentication.
Returns:
Packages are renamed by publishing a new version with the name changed in package.json
. See Creating a new package version for details.
Requests made to the previous name will forward to the new name.
Returns package.json
with dist
key added for e.g. tarball download:
{
+ "bugs": {
+ "url": "https://github.com/thedaniel/test-package/issues"
+ },
+ "dependencies": {
+ "async": "~0.2.6",
+ "pegjs": "~0.7.0",
+ "season": "~0.13.0"
+ },
+ "description": "Expand snippets matching the current prefix with \`tab\`.",
+ "dist": {
+ "tarball": "https://codeload.github.com/..."
+ },
+ "engines": {
+ "atom": "*"
+ },
+ "main": "./lib/snippets",
+ "name": "thedaniel-test-package",
+ "publishConfig": {
+ "registry": "https://..."
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package.git"
+ },
+ "version": "0.6.0"
+}
+
Creates a new package version from a git tag; requires authentication. If rename
is not true
, the name
field in package.json
must match the current package name.
Parameters:
version
key in the package.json
file at that ref. The authenticating user must have access to the package repository.Returns:
Deletes a package version; requires authentication.
Note that a version cannot be republished with a different tag if it is deleted. If you need to delete the latest version of a package for example for security reasons, you'll need to increment the version when republishing.
Returns 204 No Content
List a user's starred packages.
Return value is similar to GET /api/packages
List the authenticated user's starred packages; requires authentication.
Return value is similar to GET /api/packages
Star a package; requires authentication.
Returns a package.
Unstar a package; requires authentication.
Returns 204 No Content.
List the users that have starred a package.
Returns a list of user objects:
[{ "login": "aperson" }, { "login": "anotherperson" }]
+
This means that packages can be incredibly powerful and can change everything from the very look and feel of the entire interface to the basic operation of even core functionality.
In order to install a new package, you can use the Install tab in the now familiar Settings View. Open up the Settings View using Cmd+,Ctrl+,, click on the "Install" tab and type your search query into the box under Install Packages.
The packages listed here have been published to https://atom.io/packages which is the official registry for Atom packages. Searching on the Settings View will go to the Atom package registry and pull in anything that matches your search terms.
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Atom, it will show up in the Settings View under the "Packages" tab, along with all the preinstalled packages that come with Atom. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Atom will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Atom from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on atom.io, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
',14),A=o('You can also install packages or themes from the command line using apm
.
Tip
Check that you have apm
installed by running the following command in your terminal:
$ apm help install
+
You should see a message print out with details about the apm install
command.
If you do not, see the Installing Atom section for instructions on how to install the atom
and apm
commands for your system.
You can also install packages by using the apm install
command:
apm install <package_name>
to install the latest version.apm install <package_name>@<package_version>
to install a specific version.You can also use apm
to find new packages to install. If you run apm search
, you can search the package registry for a search term.
$ apm search coffee
+> Search Results For 'coffee' (29)
+> \u251C\u2500\u2500 build-coffee Atom Build provider for coffee, compiles CoffeeScript (1160 downloads, 2 stars)
+> \u251C\u2500\u2500 scallahan-coffee-syntax A coffee inspired theme from the guys over at S.CALLAHAN (183 downloads, 0 stars)
+> \u251C\u2500\u2500 coffee-paste Copy/Paste As : Js \u27A4 Coffee / Coffee \u27A4 Js (902 downloads, 4 stars)
+> \u251C\u2500\u2500 atom-coffee-repl Coffee REPL for Atom Editor (894 downloads, 2 stars)
+> \u251C\u2500\u2500 coffee-navigator Code navigation panel for Coffee Script (3493 downloads, 22 stars)
+> ...
+> \u251C\u2500\u2500 language-iced-coffeescript Iced coffeescript for atom (202 downloads, 1 star)
+> \u2514\u2500\u2500 slontech-syntax Dark theme for web developers ( HTML, CSS/LESS, PHP, MYSQL, javascript, AJAX, coffee, JSON ) (2018 downloads, 3 stars)
+
You can use apm view
to see more information about a specific package.
$ apm view build-coffee
+> build-coffee
+> \u251C\u2500\u2500 0.6.4
+> \u251C\u2500\u2500 https://github.com/idleberg/atom-build-coffee
+> \u251C\u2500\u2500 Atom Build provider for coffee, compiles CoffeeScript
+> \u251C\u2500\u2500 1152 downloads
+> \u2514\u2500\u2500 2 stars
+>
+> Run \`apm install build-coffee\` to install this package.
+
Text selections in Atom support a number of actions, such as scoping deletion, indentation and search actions, and marking text for actions such as quoting and bracketing.
Selections mirror many of the movement commands. They're actually exactly the same keybindings as the movement commands, but with a Shift key added in.
In addition to the cursor movement selection commands, there are also a few commands that help with selecting specific areas of content.
WARNING
Warning: This API should be considered pre-release and is subject to change.
Returns:
{
+ "name": "0.96.0",
+ "notes": "[HTML release notes]",
+ "pub_date": "2014-05-19T15:52:06.000Z",
+ "url": "https://www.atom.io/api/updates/download"
+}
+
Note
Please note that its possible this is outdated, as its original version was published by @'percova' on Nov 12, 2019.
Grammar | Selector | Provider | Status |
---|---|---|---|
All | * | SymbolProvider | Default Provider |
All | * | FuzzyProvider | Deprecated |
If you're still looking to save some typing time, Atom also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
',6),u={href:"https://github.com/atom/autocomplete-plus",target:"_blank",rel:"noopener noreferrer"};function h(f,d){const o=c("ExternalLinkIcon");return n(),s("div",null,[m,t("p",null,[e("The Autocomplete functionality is implemented in the "),t("a",u,[e("autocomplete-plus"),r(o)]),e(" package.")])])}const k=l(i,[["render",h],["__file","autocomplete.html.vue"]]);export{k as default}; diff --git a/assets/autocomplete.html.ec6b60fc.js b/assets/autocomplete.html.ec6b60fc.js new file mode 100644 index 0000000000..f0c5d85537 --- /dev/null +++ b/assets/autocomplete.html.ec6b60fc.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-2c165692","path":"/docs/atom-archive/using-atom/sections/autocomplete.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Autocomplete","slug":"autocomplete","link":"#autocomplete","children":[]}],"git":{"updatedTime":1664634443000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.42,"words":127},"filePathRelative":"docs/atom-archive/using-atom/sections/autocomplete.md"}');export{e as data}; diff --git a/assets/autocomplete.html.f60be63b.js b/assets/autocomplete.html.f60be63b.js new file mode 100644 index 0000000000..9657ad059e --- /dev/null +++ b/assets/autocomplete.html.f60be63b.js @@ -0,0 +1 @@ +import{_ as a}from"./autocomplete.b0bae406.js";import{_ as n,o as l,c as r,a as t,b as e,d as s,f as p,r as c}from"./app.0e1565ce.js";const i={},u=p('If you're still looking to save some typing time, Pulsar also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
',6),m={href:"https://github.com/pulsar-edit/autocomplete-plus",target:"_blank",rel:"noopener noreferrer"};function h(d,f){const o=c("ExternalLinkIcon");return l(),r("div",null,[u,t("p",null,[e("The Autocomplete functionality is implemented in the "),t("a",m,[e("autocomplete-plus"),s(o)]),e(" package.")])])}const k=n(i,[["render",h],["__file","autocomplete.html.vue"]]);export{k as default}; diff --git a/assets/bacadra-navigation-panel.e144ab63.png b/assets/bacadra-navigation-panel.e144ab63.png new file mode 100644 index 0000000000..3691c44338 Binary files /dev/null and b/assets/bacadra-navigation-panel.e144ab63.png differ diff --git a/assets/bacadra-pdf-viewer.9f160fe1.png b/assets/bacadra-pdf-viewer.9f160fe1.png new file mode 100644 index 0000000000..3b83012697 Binary files /dev/null and b/assets/bacadra-pdf-viewer.9f160fe1.png differ diff --git a/assets/bacadra-project-files.ca067687.png b/assets/bacadra-project-files.ca067687.png new file mode 100644 index 0000000000..7df91627d9 Binary files /dev/null and b/assets/bacadra-project-files.ca067687.png differ diff --git a/assets/backend-badge-made.716d80d6.png b/assets/backend-badge-made.716d80d6.png new file mode 100644 index 0000000000..6376f22aa1 Binary files /dev/null and b/assets/backend-badge-made.716d80d6.png differ diff --git a/assets/backend-badge.f36006d1.png b/assets/backend-badge.f36006d1.png new file mode 100644 index 0000000000..ee7e3b8116 Binary files /dev/null and b/assets/backend-badge.f36006d1.png differ diff --git a/assets/basic-customization.html.d03c4748.js b/assets/basic-customization.html.d03c4748.js new file mode 100644 index 0000000000..dfc3ca1ef0 --- /dev/null +++ b/assets/basic-customization.html.d03c4748.js @@ -0,0 +1,71 @@ +import{_ as t,a as i,b as r,c as l,d as c}from"./portable-mode-folder.90669a9a.js";import{_ as p,o as d,c as u,a as e,b as s,d as o,f as a,r as h}from"./app.0e1565ce.js";const g={},m=e("h2",{id:"basic-customization",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#basic-customization","aria-hidden":"true"},"#"),s(" Basic Customization")],-1),f=e("p",null,"Now that we are feeling comfortable with just about everything built into Pulsar, let's look at how to tweak it. Perhaps there is a keybinding that you use a lot but feels wrong or a color that isn't quite right for you. Pulsar is amazingly flexible, so let's go over some of the simpler flexes it can do.",-1),y=e("h3",{id:"configuring-with-cson",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#configuring-with-cson","aria-hidden":"true"},"#"),s(" Configuring with CSON")],-1),b=e("a",{href:"#style-tweaks"},"style sheet",-1),v=e("a",{href:"../core-hacking#the-init-file"},"Init Script",-1),k={href:"https://github.com/bevry/cson#what-is-cson",target:"_blank",rel:"noopener noreferrer"},w={href:"https://json.org/",target:"_blank",rel:"noopener noreferrer"},x=a(`key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Pulsar's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\\.pulsar
directory. You can open this file in an editor from the LNX: Edit > Stylesheet - MAC: Pulsar > Stylesheet - WIN: File > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Pulsar. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
',10),P={class:"custom-container tip"},C=e("p",{class:"custom-container-title"},"Tip",-1),_={href:"http://www.lesscss.org",target:"_blank",rel:"noopener noreferrer"},S=e("p",null,[s("If you prefer to use CSS instead, you can do that in the same "),e("code",null,"styles.less"),s(" file, since CSS is also valid in Less.")],-1),L=a(`Pulsar keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Pulsar keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Pulsar's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Pulsar is started. It will always be loaded last, giving you the chance to override bindings that are defined by Pulsar's core keymaps or third-party packages. You can open this file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Pulsar in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the LNX/WIN: Ctrl+. - MAC: Cmd+. key combination. It will show you what keys Pulsar saw you press and what command Pulsar executed because of that combination.
Pulsar loads configuration settings from the config.cson
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\\.pulsar
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the LNX: Edit > Config - MAC: Pulsar > Config - WIN: File > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of PulsarprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Pulsar will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \\n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Pulsar to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Pulsar to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Pulsar are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Pulsar Home directory will, by default, be called .pulsar
and will be located in the root of the home directory of the user.
An environment variable can be used to make Pulsar use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Pulsar Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Pulsar settings.
In addition to using the ATOM_HOME
environment variable, Pulsar can also be set to use "Portable Mode".
Portable Mode is most useful for taking Pulsar with you, with all your custom settings and packages, from machine to machine. This may take the form of keeping Pulsar on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Pulsar is in Portable Mode when there is a directory named .pulsar sibling to the directory in which the pulsar executable file lives. For example, the installed Pulsar directory can be placed into a Dropbox folder next to a .pulsar folder.
With such a setup, Pulsar will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Pulsar provides a command-line parameter option for setting Portable Mode.
$ pulsar --portable
+
Executing pulsar
with the --portable
option will take the .pulsar
directory you have in the default location (~/.pulsar
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Atom's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your ~/.atom
%USERPROFILE%\\.atom
directory. You can open this file in an editor from the Atom > StylesheetFile > StylesheetEdit > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing Alt+Cmd+ICtrl+Shift+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Atom. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
',5),z={class:"custom-container tip"},I=e("p",{class:"custom-container-title"},"Tip",-1),L={href:"http://www.lesscss.org",target:"_blank",rel:"noopener noreferrer"},P=e("p",null,[s("If you prefer to use CSS instead, you can do that in the same "),e("code",null,"styles.less"),s(" file, since CSS is also valid in Less.")],-1),F=o(`Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Atom keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Atom's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Atom is started. It will always be loaded last, giving you the chance to override bindings that are defined by Atom's core keymaps or third-party packages. You can open this file in an editor from the Atom > KeymapFile > KeymapEdit > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Atom in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the Cmd+.Ctrl+. key combination. It will show you what keys Atom saw you press and what command Atom executed because of that combination.
Atom loads configuration settings from the config.cson
file in your ~/.atom
%USERPROFILE%\\.atom
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the Atom > ConfigFile > ConfigEdit > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of AtomprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \\n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Atom to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press Cmd+Shift+PCtrl+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and press Alt+Cmd+Pchoose "Editor: Log Cursor Scope" in the Command Palette to show all scopes for the current position of the cursor. The scope mentioned top most is always the language for this kind of file, the scopes following are specific to the cursor position:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Atom to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Atom are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Atom Home directory will, by default, be called .atom
and will be located in the root of the home directory of the user.
An environment variable can be used to make Atom use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Atom Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Atom settings.
In addition to using the ATOM_HOME
environment variable, Atom can also be set to use "Portable Mode".
Portable Mode is most useful for taking Atom with you, with all your custom setting and packages, from machine to machine. This may take the form of keeping Atom on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Atom is in Portable Mode when there is a directory named .atom sibling to the directory in which the atom executable file lives. For example, the installed Atom directory can be placed into a Dropbox folder next to a .atom folder.
With such a setup, Atom will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Atom provides a command-line parameter option for setting Portable Mode.
$ atom --portable
+
Executing atom with the --portable
option will take the .atom
directory you have in the default location (~/.atom
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
YYYYMMDD-<author>-<title>.md
e.g 20221031-CreativeUsername-ThisIsMyBlogPost.md
title
, author
, date
, category
and tag
as the minimum as the others will default to false. title
- String: The displayed title of the post, consider this as H1
author
- String: The name of the author to be displayed.date
- Date (ISO 8601): Allows display of date as well as enabling sorting on the timeline, set to the same as your filename date but with hyphens (e.g. 2022-10-31
).category
- String (multiline): Enables filtering by category, this should be based on the subject of the post e.g. release
, dev log
, announcement
. This is a multiline field if you want to set more than one category.tag
- String (multiline): Enables filtering by tags, this should be based on the content of the post and areas it touches on e.g. website
, editor
, config
.sticky
- Bool: Enables "pinning" on thestar
- Bool: Enables use of the star
category for any important articles we want to remain visible.article
- Bool: You probably won't want to use this but setting this to false
will exclude this page from appearing. This is set on the "example" blog post intentionally.<!-- more -->
. Anything above the comment will be treated as the excerpt and anything underneath will be the content of the post. The website itself has a number of features which the aforementioned frontmatter fields can influence.
There is a "selector" at the top of the site which currently reads All
, Star
, Slides
, Encrypted
. The only ones we use are the first two and currently the theme doesn't support configuration of this. If necessary we may be able to hide it with CSS.
Any article with Star
set to true
will be shown in the star
category.
On the right bar are 4 counters/links and filters for Articles
, Category
, Tag
and Timeline
.Category
and Tag
are used to filter articles depending on the categories submitted in the respective frontmatter fields. The Timeline
allows views of blog posts over time according to the dates set in the date
frontmatter field.
Just ask in Discord or GH Discussions and ping the @documentation
team.
This is very much barebones default config so please let us know if you have any suggestions as to how we can improve it.
',7);function k(S,B){const i=a("ExternalLinkIcon"),n=a("RouterLink");return d(),r("div",null,[u,t("p",null,[e("This is a guide on how to add a blog post to the website which will be shown on "),t("a",g,[e("https://pulsar-edit.dev/article/"),o(i)]),e(".")]),t("p",null,[e("We are using the "),t("a",p,[e("Vuepress Blog Plugin"),o(i)]),e(" which comes as part of our Vuepress Hope Theme with some light configuration to suit our purposes.")]),t("p",null,[e("This is all implemented in the main "),t("a",m,[e("website repository"),o(i)]),e(".")]),b,t("ul",null,[f,t("li",null,[e("The rest of the post is just standard Markdown - we are currently only supporting the standard features as per the "),t("a",w,[e("MDEnhance"),o(i)]),e(" plugin but if we need more features such as GFM then please discuss and we can look at adding it to the website.")]),y,_]),t("p",null,[e("See "),t("a",v,[e("example post"),o(i)]),e(" with everything above.")]),x,t("p",null,[e("See "),o(n,{to:"/docs/resources/website/#building-the-website"},{default:c(()=>[e("building")]),_:1})]),T])}const E=l(h,[["render",k],["__file","blog-guide.html.vue"]]);export{E as default}; diff --git a/assets/blog-guide.html.8cda1ca4.js b/assets/blog-guide.html.8cda1ca4.js new file mode 100644 index 0000000000..e77bf958cd --- /dev/null +++ b/assets/blog-guide.html.8cda1ca4.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-c8081340","path":"/docs/resources/website/sections/blog-guide.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"This is a guide on how to add a blog post to the website which will be shown on\\nhttps://pulsar-edit.dev/article/
We are using the Vuepress Blog Plugin
This is all implemented in the main website repository
YYYYMMDD-<author>-<title>.md
e.g 20221031-CreativeUsername-ThisIsMyBlogPost.md
title
, author
, date
, category
\\nand tag
as the minimum as the others will default to false.\\ntitle
- String: The displayed title of the post, consider this as H1
author
- String: The name of the author to be displayed.date
- Date (ISO 8601): Allows display of date as well as enabling\\nsorting on the timeline, set to the same as your filename date but with\\nhyphens (e.g. 2022-10-31
).category
- String (multiline): Enables filtering by category, this should be based on the\\nsubject of the post e.g. release
, dev log
, announcement
. This is a\\nmultiline field if you want to set more than one category.tag
- String (multiline): Enables filtering by tags, this should be based on the content of\\nthe post and areas it touches on e.g. website
, editor
, config
.sticky
- Bool: Enables "pinning" on thestar
- Bool: Enables use of the star
category for any important articles\\nwe want to remain visible.article
- Bool: You probably won't want to use this but setting this to false
\\nwill exclude this page from appearing. This is set on the "example" blog\\npost intentionally.To build the application so you can start hacking on the core you will need to download the source code to your local machine and cd
to the pulsar directory:
git clone https://github.com/pulsar-edit/pulsar.git && cd pulsar
+
nvm install
+corepack enable
+
If Node.js is already installed, run the following to make sure the correct version of Node.js is being used (see requirements):
nvm use
+node -v
+
Run the following to initialize and update the submodules:
git submodule init && git submodule update
+
Now install and build Pulsar & ppm:
yarn install
+yarn build
+yarn build:apm
+
Start Pulsar!
yarn start
+
These instructions will also build ppm
(Pulsar Package Manager) but it will require some additional configuration for use.
The following will allow you to build Pulsar as a stand alone binary or installer. After running you will find your your built application in pulsar/binaries
.
The build script will automatically build for your system's CPU architecture, for example building on an x86_64
CPU will produce binaries for x86_64
, building on arm64
will only produce binaries for arm64
.
It is not possible to "cross-build" for different OSs. For Linux binaries you must build from a Linux machine, macOS binaries must be built from macOS etc. Your OS is detected automatically and the script will build the correct binaries for it.
`,14),D=e("p",null,[n("By default running "),e("code",null,"yarn dist"),n(" will attempt to create "),e("code",null,"appimage"),n(" (for most Linux distributions), "),e("code",null,"deb"),n(" (for Debian or Ubuntu based distributions) and "),e("code",null,"rpm"),n(" (for Red Hat or Fedora based distributions) binaries but you can select the actual target you want to build by appending the above targets to the command. e.g.:")],-1),H=e("ul",null,[e("li",null,[e("code",null,"yarn dist appimage")]),e("li",null,[e("code",null,"yarn dist deb")]),e("li",null,[e("code",null,"yarn dist rpm")]),e("li",null,[e("code",null,"yarn dist targz")])],-1),W=e("p",null,[e("code",null,"yarn dist"),n(" will create a "),e("code",null,"dmg"),n(" installer, there are currently no additional targets for macOS.")],-1),X=e("p",null,"As noted above this builds for your current CPU architecture. i.e. on an Intel Mac this will create Intel binaries, on Apple silicon (M1, M2 etc.) this will create Apple silicon binaries.",-1),G=e("p",null,[n("By default running "),e("code",null,"yarn dist"),n(" will attempt to create an "),e("code",null,"NSIS"),n(" installer as well as a "),e("code",null,"Portable"),n(" executable which does not require installation. If you only wish to build one then you can specify it by appending the above targets to the command e.g.:")],-1),Y=e("ul",null,[e("li",null,[e("code",null,"yarn dist nsis")]),e("li",null,[e("code",null,"yarn dist portable")])],-1);function J(K,Q){const s=d("ExternalLinkIcon"),r=d("Tabs");return p(),b("div",null,[m,v,e("p",null,[n("The Pulsar application code can be found in the "),e("a",g,[n("pulsar-edit/pulsar"),a(s)]),n(" repository.")]),_,f,e("ul",null,[e("li",null,[n("Node.js (version specified in "),e("a",k,[n("pulsar/.nvmrc"),a(s)]),n(", recommended installation is via "),e("a",y,[n("nvm"),a(s)]),n(")")]),x,w,P,S,e("li",null,[e("a",I,[n("Libsecret"),a(s)]),n(" development headers")])]),A,a(r,{id:"50",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"core-hacking"},{tab0:i(({title:l,value:t,isActive:o})=>[N,O,T,q,L,B,j,C]),tab1:i(({title:l,value:t,isActive:o})=>[U]),tab2:i(({title:l,value:t,isActive:o})=>[e("p",null,[n("Firstly install "),e("a",E,[n("Visual Studio"),a(s)]),n(" from Microsoft.")])]),_:1}),F,e("p",null,[n("Install Node.js (using "),M,n(" - see above) and enable corepack (for "),V,n("). This will install the version of Node.js specified in "),e("a",z,[n("pulsar/.nvmrc"),a(s)]),n(":")]),R,a(r,{id:"120",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"core-hacking"},{tab0:i(({title:l,value:t,isActive:o})=>[D,H]),tab1:i(({title:l,value:t,isActive:o})=>[W,X]),tab2:i(({title:l,value:t,isActive:o})=>[G,Y]),_:1})])}const $=u(h,[["render",J],["__file","building-pulsar.html.vue"]]);export{$ as default}; diff --git a/assets/building-pulsar.html.b0e5a0ea.js b/assets/building-pulsar.html.b0e5a0ea.js new file mode 100644 index 0000000000..59ce8ca150 --- /dev/null +++ b/assets/building-pulsar.html.b0e5a0ea.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-e880b502","path":"/docs/launch-manual/sections/core-hacking/sections/building-pulsar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Building Pulsar","slug":"building-pulsar","link":"#building-pulsar","children":[{"level":3,"title":"Requirements and dependencies","slug":"requirements-and-dependencies","link":"#requirements-and-dependencies","children":[]},{"level":3,"title":"Building and running the application","slug":"building-and-running-the-application","link":"#building-and-running-the-application","children":[]},{"level":3,"title":"Building binaries","slug":"building-binaries","link":"#building-binaries","children":[]}]}],"git":{"updatedTime":1673050223000,"contributors":[{"name":"Andrew Smith","email":"92001731+CatPerson136@users.noreply.github.com","commits":9},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":5}]},"readingTime":{"minutes":2.21,"words":662},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/building-pulsar.md"}');export{e as data}; diff --git a/assets/building.html.84c6f242.js b/assets/building.html.84c6f242.js new file mode 100644 index 0000000000..932b456c48 --- /dev/null +++ b/assets/building.html.84c6f242.js @@ -0,0 +1,6 @@ +import{_ as o,o as s,c as r,a as e,b as t,d as a,f as i,r as d}from"./app.0e1565ce.js";const l={},h=e("h2",{id:"building-the-website",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#building-the-website","aria-hidden":"true"},"#"),t(" Building the website")],-1),c=e("h3",{id:"prerequisites",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#prerequisites","aria-hidden":"true"},"#"),t(" Prerequisites")],-1),u={href:"https://github.com/nvm-sh/nvm",target:"_blank",rel:"noopener noreferrer"},p=e("li",null,"git",-1),b={href:"https://pnpm.io/",target:"_blank",rel:"noopener noreferrer"},m=e("code",null,"corepack enable",-1),f=e("h3",{id:"clone-the-repository-and-install",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#clone-the-repository-and-install","aria-hidden":"true"},"#"),t(" Clone the repository and install")],-1),g={href:"https://github.com/pulsar-edit/pulsar-edit.github.io",target:"_blank",rel:"noopener noreferrer"},v=i(`git clone https://github.com/pulsar-edit/pulsar-edit.github.io
+
+cd pulsar-edit.github.io
+
+pnpm install
+
Once installed there are a number of scripts available to help you develop and build the site. Just prefix each command with pnpm
. e,g, pnpm dev
.
dev
Starts a watch task that will rebuild VuePress whenever a change has been made to the included Markdown and JavaScript files. Additionally, it launches the development server to test the results in the browser.
build
Creates an optimized production build.
format
Note: This task will run automatically on every commit, so it can be ignored in most cases
lint
Lints all Markdown files in the repository.
Note: This task will run automatically on every commit, so it can be ignored in most cases
Whilst dev
does run a watch task, not everything will be updated and some changes will require you to shut down the server and start it again. For example adding @include
files to another file will not rebuild automatically.
If you wish to add new files from another repository via alias or @include
then you will need to run pnpm update
to get the latest version of the repository - the pnpm-lock.yml
file will also be updated and must be part of any commit.
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with \`config.get\`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
The atom.config
database is populated on startup from LNX/MAC: ~/.pulsar/config.cson
- WIN: %USERPROFILE%\\.pulsar\\config.cson
but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with \`config.get\`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
The atom.config
database is populated on startup from ~/.atom/config.cson
%USERPROFILE%\\.atom\\config.cson
, but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
For example, if you want to make changes to the tree-view
package, fork the repo on your github account, then clone it:
$ git clone git@github.com:<em>your-username</em>/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ apm install
+> Installing modules \u2713
+
Now you can link it to development mode so when you run an Atom window with atom --dev
, you will use your fork instead of the built in package:
$ apm link -d
+
Editing a package in Atom is a bit of a circular experience: you're using Atom to modify itself. What happens if you temporarily break something? You don't want the version of Atom you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the "Application: Open Dev" command. You can also run dev mode from the command line with atom --dev
.
To load your package in development mode, create a symlink to it in ~/.atom/dev/packages
. This occurs automatically when you clone the package with apm develop
. You can also run apm link --dev
and apm unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running apm update
after pulling any upstream changes.
The first step is creating your own clone. For some packages, you may also need to install the requirements necessary for building Pulsar in order to run pulsar -p install
.
For example, if you want to make changes to the tree-view
package, fork the repo on your GitHub account, then clone it:
$ git clone https://github.com/pulsar-edit/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ pulsar -p install
+> Installing modules \u2713
+
Now you can link it to development mode so when you run an Pulsar window with pulsar -p --dev
, you will use your fork instead of the built in package:
$ pulsar -p link -d
+
Editing a package in Pulsar is a bit of a circular experience: you're using Pulsar to modify itself. What happens if you temporarily break something? You don't want the version of Pulsar you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the Application: Open Dev
command. You can also run dev mode from the command line with pulsar --dev
.
To load your package in development mode, create a symlink to it in LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\dev\\packages
. This occurs automatically when you clone the package with pulsar -p develop
. You can also run pulsar -p link --dev
and pulsar -p unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running pulsar -p update
after pulling any upstream changes.
You can convert the R bundle with the following command:
$ apm init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the apm link
command, your new package is ready to use. Launch Atom and open a .r
file in the editor to see it in action!
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ apm init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Atom and opening the Settings View with the Atom > PreferencesFile > PreferencesEdit > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
`,6);function S(A,C){const n=h("ExternalLinkIcon");return i(),s("div",null,[l,t("p",null,[e("It's possible that you have themes or grammars from "),t("a",d,[e("TextMate"),o(n)]),e(" that you like and use and would like to convert to Atom. If so, you're in luck because there are tools to help with the conversion.")]),m,u,t("p",null,[e("Let's convert the TextMate bundle for the "),t("a",p,[e("R"),o(n)]),e(" programming language. You can find other existing TextMate bundles "),t("a",g,[e("on GitHub"),o(n)]),e(".")]),f,t("p",null,[e("This section will go over how to convert a "),t("a",_,[e("TextMate"),o(n)]),e(" theme to an Atom theme.")]),v,t("p",null,[e("TextMate themes use "),t("a",b,[e("plist"),o(n)]),e(" files while Atom themes use "),t("a",x,[e("CSS"),o(n)]),e(" or "),t("a",w,[e("Less"),o(n)]),e(" to style the UI and syntax in the editor.")]),y,T,t("p",null,[e("Download the theme you wish to convert, you can browse existing TextMate themes on the "),t("a",k,[e("TextMate website"),o(n)]),e(".")]),M])}const I=r(c,[["render",S],["__file","converting-from-textmate.html.vue"]]);export{I as default}; diff --git a/assets/converting-from-textmate.html.3a2f1029.js b/assets/converting-from-textmate.html.3a2f1029.js new file mode 100644 index 0000000000..42894c33ad --- /dev/null +++ b/assets/converting-from-textmate.html.3a2f1029.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-38bce125","path":"/docs/atom-archive/hacking-atom/sections/converting-from-textmate.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Converting from TextMate","slug":"converting-from-textmate","link":"#converting-from-textmate","children":[]}],"git":{"updatedTime":1664050274000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":1.31,"words":394},"filePathRelative":"docs/atom-archive/hacking-atom/sections/converting-from-textmate.md"}');export{e as data}; diff --git a/assets/converting-from-textmate.html.3ede2cec.js b/assets/converting-from-textmate.html.3ede2cec.js new file mode 100644 index 0000000000..f55ab39860 --- /dev/null +++ b/assets/converting-from-textmate.html.3ede2cec.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-f3d252b4","path":"/docs/launch-manual/sections/core-hacking/sections/converting-from-textmate.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Converting from TextMate","slug":"converting-from-textmate","link":"#converting-from-textmate","children":[{"level":3,"title":"Converting a TextMate Grammar Bundle","slug":"converting-a-textmate-grammar-bundle","link":"#converting-a-textmate-grammar-bundle","children":[]},{"level":3,"title":"Converting a TextMate Syntax Theme","slug":"converting-a-textmate-syntax-theme","link":"#converting-a-textmate-syntax-theme","children":[]}]}],"git":{"updatedTime":1670466847000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":1.23,"words":369},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/converting-from-textmate.md"}');export{e as data}; diff --git a/assets/converting-from-textmate.html.5e63d586.js b/assets/converting-from-textmate.html.5e63d586.js new file mode 100644 index 0000000000..c0ab4693b6 --- /dev/null +++ b/assets/converting-from-textmate.html.5e63d586.js @@ -0,0 +1,3 @@ +import{_ as r,o as s,c as i,a as t,b as e,d as a,f as o,r as h}from"./app.0e1565ce.js";const l={},c=t("h2",{id:"converting-from-textmate",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#converting-from-textmate","aria-hidden":"true"},"#"),e(" Converting from TextMate")],-1),d={href:"https://macromates.com",target:"_blank",rel:"noopener noreferrer"},m=t("h3",{id:"converting-a-textmate-grammar-bundle",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#converting-a-textmate-grammar-bundle","aria-hidden":"true"},"#"),e(" Converting a TextMate Grammar Bundle")],-1),u=t("p",null,"Converting a TextMate bundle will allow you to use its editor preferences, snippets, and colorization inside Pulsar.",-1),p={href:"https://en.wikipedia.org/wiki/R_(programming_language)",target:"_blank",rel:"noopener noreferrer"},g={href:"https://github.com/textmate",target:"_blank",rel:"noopener noreferrer"},f=o(`You can convert the R bundle with the following command:
$ pulsar -p init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the pulsar -p link
command, your new package is ready to use. Launch Pulsar and open a .r
file in the editor to see it in action!
The utility that converts the theme first parses the theme's plist file and then creates comparable CSS rules and properties that will style Pulsar similarly.
Download the theme you wish to convert.
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ pulsar -p init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Pulsar and opening the Settings View with the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
`,9);function k(T,M){const n=h("ExternalLinkIcon");return s(),i("div",null,[c,t("p",null,[e("It's possible that you have themes or grammars from "),t("a",d,[e("TextMate"),a(n)]),e(" that you like and use and would like to convert to Pulsar. If so, you're in luck because there are tools to help with the conversion.")]),m,u,t("p",null,[e("Let's convert the TextMate bundle for the "),t("a",p,[e("R"),a(n)]),e(" programming language. You can find other existing TextMate bundles "),t("a",g,[e("on GitHub"),a(n)]),e(".")]),f,t("p",null,[e("This section will go over how to convert a "),t("a",v,[e("TextMate"),a(n)]),e(" theme to an Pulsar theme.")]),_,t("p",null,[e("TextMate themes use "),t("a",b,[e("plist"),a(n)]),e(" files while Pulsar themes use "),t("a",x,[e("CSS"),a(n)]),e(" or "),t("a",y,[e("Less"),a(n)]),e(" to style the UI and syntax in the editor.")]),w])}const C=r(l,[["render",k],["__file","converting-from-textmate.html.vue"]]);export{C as default}; diff --git a/assets/cpu-profile-done.c24435bc.js b/assets/cpu-profile-done.c24435bc.js new file mode 100644 index 0000000000..2c0ea28106 --- /dev/null +++ b/assets/cpu-profile-done.c24435bc.js @@ -0,0 +1 @@ +const s="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKIAAAA0CAIAAACGr3QSAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAD41JREFUeAHtnNt33DaShwGQBLtbasm62Y41tifJJpnZnJx93Zf9/8/ZMy/zkniTiR3fLVn3vvEGYD8QMt2SZUfRhd0TGYei0CAAFuqHAgqFAuX9h1+Kz+HPzoF4rhropqghLqd+TsdJbnKGSPO0iUwV/RwVaj55EMDj3sB5Jp2ffnpmkZuZOC/SHACTztHvEue6zionjBSFlLlUFXjLE4IaOaedS52LnbNSZFKWUlkhbJ3tRNabCezJVs8LzIEq4EmsXTFms8o71k5ktBdF23EyUpEV7hhp57sET9eNWanKJWNKJbeSZD9KxlIGmE+28fMvMS8wB2lWyLGxG1XxbTZcK4pRlOwk+k2S7kdRrpBpP8UoYWPhlipzryxvV5N+UQ11HLlFo1URRWWN6aykuZlEmggDTE3zez1jJrTNC8w1N3ynW7Dmbln8fZzdN6IU+VDk+2pwkMj9JJlEntrEVgvGrlTVRimWndBCvKnMWEZHKj6KokmoaKb3gHEAOBBCnB46E4ADAfMCMyyAO1xGKaMkkVSIFSFuCzGxYpi7/bzIVAHRiRMLTvSFWBK+W2SwzwnG7UJJU7epZW4GsgOokScJpUF0hes4h5KRCBdZyVC0r+RIegpDzpaJnBeYAYiWo3DtxvFT2/mp14tH479aD2enhrMHg2oOkQ2iwzUW4pUQv3T1L+nCy0RnSrXMviAr4Q516IMLwq0Js2nMbVOtVsWSsT0Xb8f6H6n+NY6H72CeLthCfI5gprWVlJWQKFwTFeX8rOUbEWHEQ7inAzJEQDgyKYaROoiiQURGx19rwdXLPbT9JQeWJgVR6/rCrFu7afLbZbZamCUjelY+6916quPXjEyCmciHlrvjfMHsVSzhusLeKSd3rJdj/3vqXkff3xIh1p3YKPKudahvLTMPwtD/l6z5ocq+KkYb2WSxFKlhFeApp1+iN0Ahw3hiKhJJqRv4nv7WYvMCc8CS1TALqluV2SgqJmZ4FNLP7Ps8IgMz9Kpxy9Z0rfXTc4vrZjAGp66rHhSj74eTu4VYFCIMJwzggXIyeOKtlfVyMDTkzOZcK+Sz6l6nGxVaztx2y5g1ky9Xgsn4d4kjQ1eIfiXWTEHBuGZty0yMrV3MWNd52T316gZpKyRjNVfQv043/vp/z5E0BxWmb6slU4ExlE3LxIesgKdcIE1milDwyPkFFSmn2P1h2StKCe+RVoGi49Vco7pqxJpxG8nmIpPzw4zXJAC+LdpONHFeYA5EsfzoIRymgjuNKJyg94MfZCPzgqkWrKX4B8+vPaFUyX66uC0zDK6DKH0SJdJk3w7G9yqx8G4Mv3Yifu8F8wUzPd13/1qX8kLwe9TznGxcGMB95DwFzlHnObMoIRlvxko9S3om7i4IeRhHj1W8WKq1fLxW+QkFqgiByHA/Z+VXm22+YK4XSCrzFuxj1nwauMA4Mud+AaYo3mbg7XTKgVSPkvSpj8uC1Z0Qm1KN1fEQ3SY9n3hX2zA3sDURmNUENqMOo3hLd57qcVT4QY8ZDnU66GKhSMgPtGg0mMCwkDzV4o3uHMYxxUNVIWfzChJDqel3NS+9cCTUhklnR/p+SQi695o3APyOYnHhl16sYNswByoBIPAFWIjDr/csi2Mpuqa/uj0ZfT3JN51fMgXbyDRICO4AE5gUj7vJL93+r7r7No7hOPVPZ2vecjHunL/Ucf96167zF2wnZ3swB9kK+8QdZ1PjkL1SyImSmVQKG6Flj9khkUOlftbdgWQrYrSTT1a8dcmbsgMrgxxPIrEXqRdp99e09yZB9hU2ZFbPbEQeYhUXkp+8hT1p+Mhb0I8K9rg+6AeX5zK9arpjUWFI+TD98u+6cA3twQyJsBzrx5oxd6riTlnEwh5GyetYv0g0qHyXTxZstaeS3Sjei+OfO53Xie73+n1rSceaqI0XTtwHxlE0Yj9KqYGKB5Gi66yYar2qWD0PVfxj2kUtul2Ud0y1Yv1ux77Sb5kLkqSMolOQXJhx/14F24MZjEGJTZulqtwssm/HAzaaDiK13uku2Z529ofxUb8qD2L9Kuk+T9PdKGErYjvSW7iIWJ1iMqz3Lth1zpWs2MWSEnMKbgVrBjtUdq+crJTFYRJjFR8r+bAoNovxalWC8/N0weneQRwzzjMk3ECk24M5dH9U055za2VxvxQbbElV9st89F08gvUsNDF0lEVxIIrXWrxOO2+Tzm6k91Q0VNFhFJnYUwu0iPVSVa1as27Ydc7u5tkXpV1xXlkbFFW/3MEWca8Qa/Vu9IEftCe7SRKf3v749xLIS1HbKsxIEgCMlDpI9H6cY6RkZcmO8goCiImjtvWTB011rRB3i+ytznaSdDfpvEr0U5XuKW/NXLTmYZXfK4v1Mlsv8/VCrNc70+jkjBZU0i18hB1MuhSq+FCJQ50MVMJ+140N7cEchsqxil7oFJV4LNWbyXizLNett/gH0yAwgBA0LdfG6tsFwp0XIv9XqszyOtoZCja+Qf8z3P1mYsP+D3f0cIpQkBA2M9jEZP2KHG9p8aS78ChdfJUkaHYAfzND2zCj67I3XMoU7feAebcaP8xGX+Z+AAcDRBl5pUME8IJEkljmdtlUibHkIfLXzP5HjWuwh1CEAMyhJxE5EuI3LV7o3rbWL+POc60HtcfBZ5hb6uUgAWxI81utj7yjiH6dIMm7OvcwT3e6gDd3YOMesITKEG9SmvTQAGwmCPG/tPjfpbXfkg5bk4wB+Ojw0tAJWmrnnL1mmrFtkAavuVjdTqRkRQRIibDDKDbeVcSHaTAaLPHEFix5gwuQ99IVtnbdmMY4FKRPgPQg1s+Tzm9ak0gLuQPzTQ4zbn4Ng+sZgy52JSMqlVBV15joeLI+0W9uLNJtS3OQv3DH5uVRMW4B9+waZtKDUF4MD4oHmBec6RmLl9bxcY139V6m8ouRNCelZiPNDK1ckROAsYTfdeW15SshhUqY6lmq3bJVH38S4azD4Oltnlw3NrQnzYHLSLA/ImUtJkzcBwDjm3y8Yo9n0EvCgLDWs4CgQs5tINn7So0k1pV4FHlFrE1PsRNtCWrkiaRWf7QHM80CaTDmwMT9svguH9zLs1uVh2T1cmP1KYaBNAaT/x7mfx/lQL2l41/14m+pt5CPPtu0TzHryn+CMX2ao46bYJwNfhhl9503jNDRWP7yqAkXm0Gnx2SMptT8FyfySmxVVa86kOoW+xwY4JqTlRd7S0NkE5l+L60IcktiuEI2znk6+X4p2JS9KhqaCj8WaVWaIYLJ+EE5/jrL7tYHZBhXpzly+WYHpodOQ21M+Ui2K8QkGz5JOlsnVuYf48kF08Orzy5cNyy0NNBGw9sMbcPMTjAn3vYS9prdlqkVMS/i3haN/AX3gYu1P3QRFs34VnKxGVUpr+UhSRMlBlHMphZ5PgXGH3xxqCqsF4AN7Q9vbEamejPN6/zN61jdpZJzNzYTEpJIZ+lf+APcniSu6w6twgwjDiP1z3TxcdJhEUXz2LNA036Q4yiSfVN5JfmSMLBX8UKJxx39c3dxL0q0Hyxrg0kU7US4Jlx9AE76KM3RklP4ohBuxWuX3l7brB2SypLIAIabQ84OKq6+Tr6tnbfJ86eCOTSGLeRXmJddAndoLn0fl4+hVNjCNgcFuhiBYe1Uy4Fn+joTq+P6hXiZRo+6/R873Z3I71zCR7jv7WhUe6XeI+EMVc+a+6a8g+MDTvfCldKtFxN22BiZfBtrWrvFZDOPIpsOpCi9NMujKH4WJ9uRPzzHCORpq+/XdGtVmt+1gWb6wQ0WYPVkq+ql1uu2O/bD2MUDbOLCIf5l2nsRa6oF1XrRXD8AYL94vsoQZtm+qb4f7/1tUnBSBG7yDm29/xojU5BmUhD3bw6GD9SwCkt4Vnpd/Xhp5f9U9EjK7TrnnwTm42bU8hT6Lin0d/as9lWMl8heLEa1nYTEMwP8CkCF+6k89BtsXgesoGK9EyellL6e49fVeYMsnyp2qZ8OpFNR4fD0sPCnsRvdgvRpIrHxofwzTNHkwIe9SVF1zW7qdJ1A/maEvxRFHyl8rZV/5J0nk2EHG1YcW93SyVs8rk8+Db+Ou8hZj5q0sDe1E+MK6I0hwRLSPL2+CPTjKxEkm7cEUrnD2ebiZ7imyWAka0pNp19HfAaDdmAEjQn9HV5gM9GWmfp4VCVDIwohwvn/FbwEoxi+8FkZRkXSmzwNX0hhz4tDsPh0FvWHhXjUvK7JdlURZgWqmqj4WSfVLl8u/XDtwydfWWMrjzr9pzHdWqIzEq5b2mYAc92u4xvApPU3gf5S5l9k/rArCup0CFgy4n01GXQM6ovCeYipjvTwKGSGscFvhNOnXxYTPDv5eAEjBOz7JM+nX/WH49TMvMAXjP7RWf1JO1jpP1l1rqAqpSZxxGcqWPt9bJI6V03nyzQbmI8Rqr8CtmzM1/nku9HgrvV7iMyvXJ5l7/o4mYH5YS5Wyoz4ovU/G4yJkB/3TZjO1Ihn2d8mAw494P3JXMDEH8xe1wE2miRMzmT0PD5G6rwov2ta6IXXQdsp9GcDcyCCRmprb1flf432v88dqynYhO8HyhTyCuQIKPwDSLTWu9izai6Sws8QeERm1iR7NePuCHFLiP8sGLoH+zHOX3xeKAou+O9KXMt/GhIAJnL+8Icyn7/aM3POBuYgi7jR9429bYoHuQMhJHJHiCdasBZacHwSSmzUKxOGcfBulFiaAU9Bl/yYunaFeKvFTpKwu1yOq80aabyDvyizLTy9E3Xdq1JkkatNzM4E8tOJs4E50ITmxUYkPtuYOYFtW4hHXfXPhVvAk1jBV1q+Hx58VXhvQISbEAY3uggXmgsA0yd+7PWf+ZM40aqtxuqoGOVfOd8nNrJsPe7s8YXANua+0KD5vc8SZrgCukdRAlpbTrxI/KG3n5Meh94QjoMKBzExzEYbVYmbAJ9oCRMgiZny3te7On7SWfiJM1QxDoPywMao2YUcDbMxHWFX60bZnl/2t0WZnMn3tMNMxmEZ1Oxlr2mXDOCcXPVf6lMK1Ynm+w8xWcNH/8izaHEy4YtMQCz4YiMzLk7XQDvyRaK8XoDSYfuWDzRVy5WhlxzGitrw2y1q7+w5H1SvG+7ZwBzm5rBOZocHhEjBboVNAzyawZne4HcD0NQ47VjZtFZ0OK5+FCug9Q8+yE934fQNNdAj0LFDHp/xuhk53/XPeNCGORiDgpYEKg3GARgvgrWkghnQqlrRAXuUZx9qzjb4EQn5vcjX/ab+//nmOTAbmI+xqZFC4LzM1QLXYFYnHIsgifQAZD0kNo9O/K5Tfc46c5PzwzzNoxsV+X/qbiSVpvxXqQAAAABJRU5ErkJggg==",n="/assets/settings.f822be64.png",t="/assets/package-settings.ad8a3253.png",A="/assets/keybinding-resolver.befdd67c.png",a="/assets/fonts-in-use.d1baabe5.png",p="/assets/exception-notification.f8f467e7.png",e="/assets/devtools-error.d36c506a.png",M="/assets/timecop.1c72c1e3.png",r="/assets/cpu-profile-start.013307c2.png",o="/assets/cpu-profile-done.fca74677.png";export{s as _,n as a,t as b,M as c,A as d,a as e,p as f,e as g,r as h,o as i}; diff --git a/assets/cpu-profile-done.fca74677.png b/assets/cpu-profile-done.fca74677.png new file mode 100644 index 0000000000..f434068c0d Binary files /dev/null and b/assets/cpu-profile-done.fca74677.png differ diff --git a/assets/cpu-profile-start.013307c2.png b/assets/cpu-profile-start.013307c2.png new file mode 100644 index 0000000000..f28991dfa9 Binary files /dev/null and b/assets/cpu-profile-start.013307c2.png differ diff --git a/assets/creating-a-fork-of-a-core-package-in-atom-atom.html.367b286d.js b/assets/creating-a-fork-of-a-core-package-in-atom-atom.html.367b286d.js new file mode 100644 index 0000000000..d432fbff32 --- /dev/null +++ b/assets/creating-a-fork-of-a-core-package-in-atom-atom.html.367b286d.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-752e2619","path":"/docs/atom-archive/hacking-atom/sections/creating-a-fork-of-a-core-package-in-atom-atom.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Creating a Fork of a Core Package in atom/atom","slug":"creating-a-fork-of-a-core-package-in-atom-atom","link":"#creating-a-fork-of-a-core-package-in-atom-atom","children":[{"level":3,"title":"Creating Your New Package","slug":"creating-your-new-package","link":"#creating-your-new-package","children":[]},{"level":3,"title":"Merging Upstream Changes into Your Package","slug":"merging-upstream-changes-into-your-package","link":"#merging-upstream-changes-into-your-package","children":[]}]}],"git":{"updatedTime":1668310114000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":4},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":1.54,"words":462},"filePathRelative":"docs/atom-archive/hacking-atom/sections/creating-a-fork-of-a-core-package-in-atom-atom.md"}');export{a as data}; diff --git a/assets/creating-a-fork-of-a-core-package-in-atom-atom.html.8f3be404.js b/assets/creating-a-fork-of-a-core-package-in-atom-atom.html.8f3be404.js new file mode 100644 index 0000000000..aa496a4126 --- /dev/null +++ b/assets/creating-a-fork-of-a-core-package-in-atom-atom.html.8f3be404.js @@ -0,0 +1,6 @@ +import{_ as s,o as c,c as l,a as e,b as a,d as o,w as i,f as m,r}from"./app.0e1565ce.js";const p={},d=e("h2",{id:"creating-a-fork-of-a-core-package-in-atom-atom",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#creating-a-fork-of-a-core-package-in-atom-atom","aria-hidden":"true"},"#"),a(" Creating a Fork of a Core Package in atom/atom")],-1),u={href:"https://github.com/atom/atom/tree/master/packages",target:"_blank",rel:"noopener noreferrer"},g=e("code",null,"packages",-1),h={class:"custom-container tip"},k=e("p",{class:"custom-container-title"},"Tips",-1),f=e("strong",null,"Tip:",-1),_=e("h3",{id:"creating-your-new-package",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#creating-your-new-package","aria-hidden":"true"},"#"),a(" Creating Your New Package")],-1),y={href:"https://github.com/atom/atom/tree/master/packages/one-light-ui",target:"_blank",rel:"noopener noreferrer"},b={href:"https://github.com/atom/atom/archive/master.zip",target:"_blank",rel:"noopener noreferrer"},v=m(`Unzip the file to a temporary location (for example /tmp/atom
C:\\TEMP\\atom
)
Copy the contents of the desired package into a working directory for your fork
$ <span class='platform-mac platform-linux'>cp -R /tmp/atom/packages/one-light-ui ~/src/one-light-ui-plus</span><span class='platform-windows'>xcopy C:\\TEMP\\atom\\packages\\one-light-ui C:\\src\\one-light-ui-plus /E /H /K</span>
+
Create a local repository and commit the initial contents
$ cd ~/src/one-light-ui-plus
+$ git init
+$ git commit -am "Import core Atom package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
Tip
In most cases, we recommend generating a brand new package or a brand new theme as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Pulsar package.
Unzip the file to a temporary location (for example LNX/MAC: /tmp/pulsar
- WIN: C:\\TEMP\\pulsar
)
Copy the contents of the desired package into a working directory for your fork
$ git init
+$ git commit -am "Import core Pulsar package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
This syntax tree gives Atom a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Atom are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Atom: a parser and a grammar file.
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Atom package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+\u251C\u2500\u2500 LICENSE
+\u251C\u2500\u2500 README.md
+\u251C\u2500\u2500 grammars
+\u2502 \u2514\u2500\u2500 mylanguage.cson
+\u2514\u2500\u2500 package.json
+
The mylanguage.cson
file specifies how Atom should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
Next, the file should contain some fields that indicate to Atom when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Atom uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
scopes:\n '''\n "*",\n "/",\n "+",\n "-"\n ''': 'keyword.operator'\n
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:\n\n # Apply the classes `syntax--builtin` and `syntax--variable` to all\n # `identifier` nodes whose text is `require`.\n 'identifier': {exact: 'require', scopes: 'builtin.variable'},\n\n # Apply the classes `syntax--type` and `syntax--integer` to all\n # `primitive_type` nodes whose text starts with `int` or `uint`.\n 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},\n\n # Apply the classes `syntax--builtin`, `syntax--class`, and\n # `syntax--name` to `constant` nodes with the text `Array`,\n # `Hash` and `String`. For all other `constant` nodes, just\n # apply the classes `syntax--class` and `syntax--name`.\n 'constant': [\n {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},\n 'class.name'\n ]\n
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector \`call_expression > identifier\` is more
+ # specific than the selector \`identifier\`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
// HTML in a template literal
+const htmlContent = html\`<div>Hello \${name}</div>\`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All \`comment\` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # \`if_statement\` nodes are foldable if they contain an anonymous
+ # "then" token and either an \`elif_clause\` or \`else_clause\` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # \`elif_clause\` or \`else_clause\`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Atom's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or un-comment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Atom:
`,23),se={href:"https://github.com/atom/language-shellscript",target:"_blank",rel:"noopener noreferrer"},ae={href:"https://github.com/atom/language-c",target:"_blank",rel:"noopener noreferrer"},te={href:"https://github.com/atom/language-go",target:"_blank",rel:"noopener noreferrer"},oe={href:"https://github.com/atom/language-html",target:"_blank",rel:"noopener noreferrer"},ie={href:"https://github.com/atom/language-javascript",target:"_blank",rel:"noopener noreferrer"},re={href:"https://github.com/atom/language-python",target:"_blank",rel:"noopener noreferrer"},le={href:"https://github.com/atom/language-ruby",target:"_blank",rel:"noopener noreferrer"},ce={href:"https://github.com/atom/language-typescript",target:"_blank",rel:"noopener noreferrer"};function pe(de,ue){const s=l("ExternalLinkIcon");return i(),r("div",null,[p,e("p",null,[n("Atom's syntax highlighting and code folding system is powered by "),e("a",d,[n("Tree-sitter"),a(s)]),n(". Tree-sitter parsers create and maintain full "),e("a",u,[h,a(s)]),n(" representing your code.")]),m,e("p",null,[n("Tree-sitter generates parsers based on "),e("a",g,[n("context-free grammars"),a(s)]),n(" that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Atom.")]),e("p",null,[n("They can also be developed and tested at the command line, separately from Atom. Tree-sitter has "),e("a",v,[n("its own documentation page"),a(s)]),n(" on how to create these parsers. The "),e("a",f,[n("Tree-sitter GitHub organization"),a(s)]),n(" also contains a lot of example parsers that you can learn from, each in its own repository.")]),e("p",null,[n("Once you have created a parser, you need to publish it to "),e("a",k,[n("the NPM registry"),a(s)]),n(" to use it in Atom. To do this, make sure you have a "),b,n(" and "),y,n(" in your parser's "),_,n(":")]),x,e("ul",null,[w,T,e("li",null,[q,n(" - The name of the parser node module that will be used for parsing. This string will be passed directly to "),e("a",S,[j,a(s)]),n(" in order to load the parser.")]),A]),C,e("p",null,[n("Note that in this selector, we're using the "),e("a",I,[n("immediate child combinator"),a(s)]),n(" ("),N,n("). Arbitrary descendant selectors without this combinator (for example "),E,n(", which would match any "),H,n(" occurring anywhere within a "),L,n(") are currently not supported.")]),M,e("p",null,[n("You can use the "),e("a",R,[F,n(" pseudo-class"),a(s)]),n(" to select nodes based on their order within their parent. For example, this example selects "),P,n(" nodes which are the fourth (zero-indexed) child of a "),z,n(" node.")]),B,e("p",null,[n("Finally, you can use double-quoted strings in the selectors to select "),G,n(" tokens in the syntax tree, like "),J,n(" and "),O,n(". See "),e("a",U,[n("the Tree-sitter documentation"),a(s)]),n(" for more information about named vs anonymous tokens.")]),V,e("p",null,[n("If multiple selectors in the "),W,n(" object match a node, the node's classes will be decided based on the "),e("a",Y,[n("most specific"),a(s)]),n(" selector. Note that the "),$,n(" and "),D,n(" rules do "),Z,n(" affect specificity, so you may need to supply the same "),K,n(" or "),Q,n(" rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:")]),X,e("p",null,[n("For example, in JavaScript, "),e("a",ee,[n("tagged template literals"),a(s)]),n(" sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:")]),ne,e("ul",null,[e("li",null,[e("a",se,[n("Bash"),a(s)])]),e("li",null,[e("a",ae,[n("C"),a(s)])]),e("li",null,[e("a",te,[n("Go"),a(s)])]),e("li",null,[e("a",oe,[n("HTML"),a(s)])]),e("li",null,[e("a",ie,[n("JavaScript"),a(s)])]),e("li",null,[e("a",re,[n("Python"),a(s)])]),e("li",null,[e("a",le,[n("Ruby"),a(s)])]),e("li",null,[e("a",ce,[n("TypeScript"),a(s)])])])])}const me=o(c,[["render",pe],["__file","creating-a-grammar.html.vue"]]);export{me as default}; diff --git a/assets/creating-a-grammar.html.741fc286.js b/assets/creating-a-grammar.html.741fc286.js new file mode 100644 index 0000000000..4800aaab36 --- /dev/null +++ b/assets/creating-a-grammar.html.741fc286.js @@ -0,0 +1,94 @@ +import{_ as o,o as i,c as r,e as l,a as e,b as n,d as a,f as t,r as c}from"./app.0e1565ce.js";const p={},d=e("h2",{id:"creating-a-grammar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#creating-a-grammar","aria-hidden":"true"},"#"),n(" Creating a Grammar")],-1),u={href:"http://tree-sitter.github.io/tree-sitter",target:"_blank",rel:"noopener noreferrer"},h={href:"https://en.wikipedia.org/wiki/Abstract_syntax_tree",target:"_blank",rel:"noopener noreferrer"},m=e("em",null,"syntax trees",-1),g=t('This syntax tree gives Pulsar a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Pulsar are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Pulsar: a parser and a grammar file.
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Pulsar package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+\u251C\u2500\u2500 LICENSE
+\u251C\u2500\u2500 README.md
+\u251C\u2500\u2500 grammars
+\u2502 \u2514\u2500\u2500 mylanguage.cson
+\u2514\u2500\u2500 package.json
+
The mylanguage.cson
file specifies how Pulsar should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
Next, the file should contain some fields that indicate to Pulsar when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Pulsar uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
scopes:\n '''\n "*",\n "/",\n "+",\n "-"\n ''': 'keyword.operator'\n
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:\n\n # Apply the classes `syntax--builtin` and `syntax--variable` to all\n # `identifier` nodes whose text is `require`.\n 'identifier': {exact: 'require', scopes: 'builtin.variable'},\n\n # Apply the classes `syntax--type` and `syntax--integer` to all\n # `primitive_type` nodes whose text starts with `int` or `uint`.\n 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},\n\n # Apply the classes `syntax--builtin`, `syntax--class`, and\n # `syntax--name` to `constant` nodes with the text `Array`,\n # `Hash` and `String`. For all other `constant` nodes, just\n # apply the classes `syntax--class` and `syntax--name`.\n 'constant': [\n {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},\n 'class.name'\n ]\n
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector \`call_expression > identifier\` is more
+ # specific than the selector \`identifier\`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
// HTML in a template literal
+const htmlContent = html\`<div>Hello \${name}</div>\`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All \`comment\` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # \`if_statement\` nodes are foldable if they contain an anonymous
+ # "then" token and either an \`elif_clause\` or \`else_clause\` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # \`elif_clause\` or \`else_clause\`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Pulsar's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or uncomment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Pulsar:
`,23),ae={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-shellscript",target:"_blank",rel:"noopener noreferrer"},te={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-c",target:"_blank",rel:"noopener noreferrer"},oe={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-go",target:"_blank",rel:"noopener noreferrer"},ie={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-html",target:"_blank",rel:"noopener noreferrer"},re={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-javascript",target:"_blank",rel:"noopener noreferrer"},le={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-python",target:"_blank",rel:"noopener noreferrer"},ce={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-ruby",target:"_blank",rel:"noopener noreferrer"},pe={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-typescript",target:"_blank",rel:"noopener noreferrer"};function de(ue,he){const s=c("ExternalLinkIcon");return i(),r("div",null,[d,l("This may need to be thoroughly reworked if we change the tree-sitter implementation"),e("p",null,[n("Pulsar's syntax highlighting and code folding system is powered by "),e("a",u,[n("Tree-sitter"),a(s)]),n(". Tree-sitter parsers create and maintain full "),e("a",h,[m,a(s)]),n(" representing your code.")]),g,e("p",null,[n("Tree-sitter generates parsers based on "),e("a",v,[n("context-free grammars"),a(s)]),n(" that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Pulsar.")]),e("p",null,[n("They can also be developed and tested at the command line, separately from Pulsar. Tree-sitter has "),e("a",k,[n("its own documentation page"),a(s)]),n(" on how to create these parsers. The "),e("a",f,[n("Tree-sitter GitHub organization"),a(s)]),n(" also contains a lot of example parsers that you can learn from, each in its own repository.")]),e("p",null,[n("Once you have created a parser, you need to publish it to "),e("a",b,[n("the NPM registry"),a(s)]),n(" to use it in Pulsar. To do this, make sure you have a "),y,n(" and "),_,n(" in your parser's "),x,n(":")]),w,e("ul",null,[T,q,e("li",null,[S,n(" - The name of the parser node module that will be used for parsing. This string will be passed directly to "),e("a",j,[C,a(s)]),n(" in order to load the parser.")]),I]),A,e("p",null,[n("Note that in this selector, we're using the "),e("a",P,[n("immediate child combinator"),a(s)]),n(" ("),N,n("). Arbitrary descendant selectors without this combinator (for example "),E,n(", which would match any "),H,n(" occurring anywhere within a "),L,n(") are currently not supported.")]),M,e("p",null,[n("You can use the "),e("a",R,[F,n(" pseudo-class"),a(s)]),n(" to select nodes based on their order within their parent. For example, this example selects "),z,n(" nodes which are the fourth (zero-indexed) child of a "),B,n(" node.")]),G,e("p",null,[n("Finally, you can use double-quoted strings in the selectors to select "),J,n(" tokens in the syntax tree, like "),O,n(" and "),V,n(". See "),e("a",U,[n("the Tree-sitter documentation"),a(s)]),n(" for more information about named vs anonymous tokens.")]),W,e("p",null,[n("If multiple selectors in the "),Y,n(" object match a node, the node's classes will be decided based on the "),e("a",$,[n("most specific"),a(s)]),n(" selector. Note that the "),D,n(" and "),Z,n(" rules do "),K,n(" affect specificity, so you may need to supply the same "),Q,n(" or "),X,n(" rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:")]),ee,e("p",null,[n("For example, in JavaScript, "),e("a",ne,[n("tagged template literals"),a(s)]),n(" sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:")]),se,e("ul",null,[e("li",null,[e("a",ae,[n("Bash"),a(s)])]),e("li",null,[e("a",te,[n("C"),a(s)])]),e("li",null,[e("a",oe,[n("Go"),a(s)])]),e("li",null,[e("a",ie,[n("HTML"),a(s)])]),e("li",null,[e("a",re,[n("JavaScript"),a(s)])]),e("li",null,[e("a",le,[n("Python"),a(s)])]),e("li",null,[e("a",ce,[n("Ruby"),a(s)])]),e("li",null,[e("a",pe,[n("TypeScript"),a(s)])])])])}const ge=o(p,[["render",de],["__file","creating-a-grammar.html.vue"]]);export{ge as default}; diff --git a/assets/creating-a-grammar.html.d2553095.js b/assets/creating-a-grammar.html.d2553095.js new file mode 100644 index 0000000000..aa28158eda --- /dev/null +++ b/assets/creating-a-grammar.html.d2553095.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-43bb93f6","path":"/docs/launch-manual/sections/core-hacking/sections/creating-a-grammar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Creating a Grammar","slug":"creating-a-grammar","link":"#creating-a-grammar","children":[{"level":3,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[]},{"level":3,"title":"The Parser","slug":"the-parser","link":"#the-parser","children":[]},{"level":3,"title":"The Package","slug":"the-package","link":"#the-package","children":[]},{"level":3,"title":"The Grammar File","slug":"the-grammar-file","link":"#the-grammar-file","children":[]},{"level":3,"title":"Basic Fields","slug":"basic-fields","link":"#basic-fields","children":[]},{"level":3,"title":"Language Recognition","slug":"language-recognition","link":"#language-recognition","children":[]},{"level":3,"title":"Syntax Highlighting","slug":"syntax-highlighting","link":"#syntax-highlighting","children":[]},{"level":3,"title":"Language Injection","slug":"language-injection","link":"#language-injection","children":[]},{"level":3,"title":"Code Folding","slug":"code-folding","link":"#code-folding","children":[]},{"level":3,"title":"Comments","slug":"comments","link":"#comments","children":[]},{"level":3,"title":"Example Packages","slug":"example-packages","link":"#example-packages","children":[]}]}],"git":{"updatedTime":1670466847000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":7.46,"words":2237},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/creating-a-grammar.md"}');export{e as data}; diff --git a/assets/creating-a-legacy-textmate-grammar.html.87a1089f.js b/assets/creating-a-legacy-textmate-grammar.html.87a1089f.js new file mode 100644 index 0000000000..8f7bc0c43a --- /dev/null +++ b/assets/creating-a-legacy-textmate-grammar.html.87a1089f.js @@ -0,0 +1,94 @@ +import{_ as o,o as r,c as p,a as n,b as e,d as s,e as i,f as t,r as l}from"./app.0e1565ce.js";const c={},d=t('Atom's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Atom's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
Note
Note: This tutorial is a work in progress.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Atom uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Tip: Grammar packages should start with language-.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \\bFlight Manual\\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\\\bFlight Manual\\\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
Tip
Tip: All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Note: Astute readers may have noticed that the \\b
was changed to \\\\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\\\b(Flight) (Manual)\\\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the ::: note Note
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. ::: note Note
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Tip: Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\\\b(Flight) (Manual)\\\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
`,19),F={href:"https://gist.github.com/DamnedScholar/622926bcd222eb1ddc483d12103fd315",target:"_blank",rel:"noopener noreferrer"},A={href:"https://gist.github.com/Aerijo/b8c82d647db783187804e86fa0a604a1",target:"_blank",rel:"noopener noreferrer"},G=n("li",null,"http://www.apeth.com/nonblog/stories/textmatebundle.html. A blog of a programmer's experience writing a grammar package for TextMate.",-1),P={href:"https://github.com/kkos/oniguruma/blob/master/doc/RE",target:"_blank",rel:"noopener noreferrer"},O={href:"http://manual.macromates.com/en/language_grammars.html",target:"_blank",rel:"noopener noreferrer"},I={href:"https://github.com/atom/first-mate",target:"_blank",rel:"noopener noreferrer"},L=n("code",null,"first-mate",-1),q={href:"https://github.com/atom/language-python",target:"_blank",rel:"noopener noreferrer"},E={href:"https://github.com/atom/language-javascript",target:"_blank",rel:"noopener noreferrer"},j={href:"https://github.com/atom/language-html",target:"_blank",rel:"noopener noreferrer"},H={href:"https://github.com/atom?utf8=%E2%9C%93&q=language&type=source&language=",target:"_blank",rel:"noopener noreferrer"};function W(z,B){const a=l("ExternalLinkIcon");return r(),p("div",null,[d,n("p",null,[e("Grammar files are written in the "),n("a",u,[e("CSON"),s(a)]),e(" or "),n("a",h,[e("JSON"),s(a)]),e(" format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.")]),g,n("p",null,[e("The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the "),m,e(", "),k,e(", "),v,e(", and "),b,e(" folders. Furthermore, in "),y,e(", remove the "),f,e(" section. Now create a new folder called "),w,e(", and inside that a file called "),_,e(". This is the main file that we will be working with - start by populating it with a "),n("a",x,[e("boilerplate template"),s(a)]),e(". Now let's go over what each key means.")]),T,n("p",null,[M,e(" is where your regex is contained, and "),N,e(" is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in "),n("a",S,[e("Section 12.4 of the TextMate Manual"),s(a)]),e(".")]),C,n("ul",null,[n("li",null,[n("a",F,[e("DamnedScholar's Gist"),s(a)]),e(". Provides a template of most keys, each with a short comment explaining their function.")]),n("li",null,[n("a",A,[e("Aerijo's Gist"),s(a)]),e(". [Work in Progress] Another guide that attempts to fully explain making a grammar package for users of all levels.")]),G,n("li",null,[n("a",P,[e("Oniguruma docs"),s(a)]),e(". The documentation for the regex engine Atom uses.")]),n("li",null,[n("a",O,[e("TextMate Section 12"),s(a)]),e(". Atom uses the same principles as laid out here, including the list of acceptable scopes.")]),n("li",null,[n("a",I,[L,s(a)]),e(". Not necessary to write a grammar, but a good technical reference for what Atom is doing behind the scenes.")]),n("li",null,[e("Look at any existing packages, such as the ones for "),n("a",q,[e("Python"),s(a)]),e(", "),n("a",E,[e("JavaScript"),s(a)]),e(", "),n("a",j,[e("HTML"),s(a)]),e(", "),n("a",H,[e("and more"),s(a)]),e(".")])]),i(` +TODO: +* \`repository\` and including from repository patterns +* Wrap-up +* Implement Package Generator functionality to generate a grammar +* Intermediate + advanced grammar tutorials +`)])}const V=o(c,[["render",W],["__file","creating-a-legacy-textmate-grammar.html.vue"]]);export{V as default}; diff --git a/assets/creating-a-legacy-textmate-grammar.html.895bfcba.js b/assets/creating-a-legacy-textmate-grammar.html.895bfcba.js new file mode 100644 index 0000000000..9198386422 --- /dev/null +++ b/assets/creating-a-legacy-textmate-grammar.html.895bfcba.js @@ -0,0 +1,94 @@ +import{_ as o,o as r,c as p,a as n,b as e,d as s,e as i,f as t,r as l}from"./app.0e1565ce.js";const c={},d=t('Pulsar's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Pulsar's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Pulsar uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Grammar packages should start with language-.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \\bFlight Manual\\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\\\bFlight Manual\\\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
Tip
All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Astute readers may have noticed that the \\b
was changed to \\\\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\\\b(Flight) (Manual)\\\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the {{#note}}
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. {{#note}}
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\\\b(Flight) (Manual)\\\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
`,19),F={href:"https://gist.github.com/DamnedScholar/622926bcd222eb1ddc483d12103fd315",target:"_blank",rel:"noopener noreferrer"},P={href:"https://gist.github.com/Aerijo/b8c82d647db783187804e86fa0a604a1",target:"_blank",rel:"noopener noreferrer"},G={href:"http://www.apeth.com/nonblog/stories/textmatebundle.html",target:"_blank",rel:"noopener noreferrer"},A={href:"https://github.com/kkos/oniguruma/blob/master/doc/RE",target:"_blank",rel:"noopener noreferrer"},O={href:"http://manual.macromates.com/en/language_grammars.html",target:"_blank",rel:"noopener noreferrer"},I={href:"https://github.com/pulsar-edit/first-mate",target:"_blank",rel:"noopener noreferrer"},L=n("code",null,"first-mate",-1),j={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-python",target:"_blank",rel:"noopener noreferrer"},q={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-javascript",target:"_blank",rel:"noopener noreferrer"},E={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-html",target:"_blank",rel:"noopener noreferrer"},H={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages",target:"_blank",rel:"noopener noreferrer"};function W(z,B){const a=l("ExternalLinkIcon");return r(),p("div",null,[d,n("p",null,[e("Grammar files are written in the "),n("a",u,[e("CSON"),s(a)]),e(" or "),n("a",h,[e("JSON"),s(a)]),e(" format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.")]),g,n("p",null,[e("The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the "),m,e(", "),k,e(", "),v,e(", and "),b,e(" folders. Furthermore, in "),y,e(", remove the "),f,e(" section. Now create a new folder called "),w,e(", and inside that a file called "),_,e(". This is the main file that we will be working with - start by populating it with a "),n("a",x,[e("boilerplate template"),s(a)]),e(". Now let's go over what each key means.")]),T,n("p",null,[M,e(" is where your regex is contained, and "),N,e(" is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in "),n("a",S,[e("Section 12.4 of the TextMate Manual"),s(a)]),e(".")]),C,n("ul",null,[n("li",null,[n("a",F,[e("DamnedScholar's Gist"),s(a)]),e(". Provides a template of most keys, each with a short comment explaining their function.")]),n("li",null,[n("a",P,[e("Aerijo's Gist"),s(a)]),e(". Another guide that attempts to fully explain making a grammar package for users of all levels.")]),n("li",null,[n("a",G,[e("http://www.apeth.com/nonblog/stories/textmatebundle.html"),s(a)]),e(". A blog of a programmer's experience writing a grammar package for TextMate.")]),n("li",null,[n("a",A,[e("Oniguruma docs"),s(a)]),e(". The documentation for the regex engine Pulsar uses.")]),n("li",null,[n("a",O,[e("TextMate Section 12"),s(a)]),e(". Pulsar uses the same principles as laid out here, including the list of acceptable scopes.")]),n("li",null,[n("a",I,[L,s(a)]),e(". Not necessary to write a grammar, but a good technical reference for what Pulsar is doing behind the scenes.")]),n("li",null,[e("Look at any existing packages, such as the ones for "),n("a",j,[e("Python"),s(a)]),e(", "),n("a",q,[e("JavaScript"),s(a)]),e(", "),n("a",E,[e("HTML"),s(a)]),e(", "),n("a",H,[e("and more"),s(a)]),e(".")])]),i(` (This is left from the original Atom flight-manual) +TODO: +* \`repository\` and including from repository patterns +* Wrap-up +* Implement Package Generator functionality to generate a grammar +* Intermediate + advanced grammar tutorials +`)])}const V=o(c,[["render",W],["__file","creating-a-legacy-textmate-grammar.html.vue"]]);export{V as default}; diff --git a/assets/creating-a-legacy-textmate-grammar.html.b6783a7b.js b/assets/creating-a-legacy-textmate-grammar.html.b6783a7b.js new file mode 100644 index 0000000000..e30e857828 --- /dev/null +++ b/assets/creating-a-legacy-textmate-grammar.html.b6783a7b.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1032ada6","path":"/docs/launch-manual/sections/core-hacking/sections/creating-a-legacy-textmate-grammar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Creating a Legacy TextMate Grammar","slug":"creating-a-legacy-textmate-grammar","link":"#creating-a-legacy-textmate-grammar","children":[{"level":3,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[]},{"level":3,"title":"Create the Package","slug":"create-the-package","link":"#create-the-package","children":[]},{"level":3,"title":"Adding Patterns","slug":"adding-patterns","link":"#adding-patterns","children":[]},{"level":3,"title":"Begin/End Patterns","slug":"begin-end-patterns","link":"#begin-end-patterns","children":[]},{"level":3,"title":"Repositories and the Include keyword, or how to avoid duplication","slug":"repositories-and-the-include-keyword-or-how-to-avoid-duplication","link":"#repositories-and-the-include-keyword-or-how-to-avoid-duplication","children":[]},{"level":3,"title":"Where to Go from Here","slug":"where-to-go-from-here","link":"#where-to-go-from-here","children":[]}]}],"git":{"updatedTime":1670466847000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":5.27,"words":1581},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/creating-a-legacy-textmate-grammar.md"}');export{e as data}; diff --git a/assets/creating-a-legacy-textmate-grammar.html.ed46d264.js b/assets/creating-a-legacy-textmate-grammar.html.ed46d264.js new file mode 100644 index 0000000000..edb5218c9c --- /dev/null +++ b/assets/creating-a-legacy-textmate-grammar.html.ed46d264.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-41063e28","path":"/docs/atom-archive/hacking-atom/sections/creating-a-legacy-textmate-grammar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Creating a Legacy TextMate Grammar","slug":"creating-a-legacy-textmate-grammar","link":"#creating-a-legacy-textmate-grammar","children":[]}],"git":{"updatedTime":1664050274000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":5.26,"words":1579},"filePathRelative":"docs/atom-archive/hacking-atom/sections/creating-a-legacy-textmate-grammar.md"}');export{a as data}; diff --git a/assets/creating-a-theme.html.22be0ae0.js b/assets/creating-a-theme.html.22be0ae0.js new file mode 100644 index 0000000000..81cac5c8e3 --- /dev/null +++ b/assets/creating-a-theme.html.22be0ae0.js @@ -0,0 +1,12 @@ +import{_ as i,a as l,b as d}from"./theme-side-by-side.33cf8d5d.js";import{_ as c}from"./dev-tools.1b7b2813.js";import{_ as h,o as u,c as p,a as e,b as t,d as s,e as m,w as g,f as o,r as n}from"./app.0e1565ce.js";const b={},f=e("h2",{id:"creating-a-theme",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#creating-a-theme","aria-hidden":"true"},"#"),t(" Creating a Theme")],-1),y={href:"http://lesscss.org/",target:"_blank",rel:"noopener noreferrer"},k=o('Pulsar supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
',5),v={href:"https://speakerdeck.com/danmatthews/less-css",target:"_blank",rel:"noopener noreferrer"},w=o('package.json
(as covered in Pulsar package.json
). This file is used to help distribute your theme to Pulsar users.package.json
must contain a theme
key with a value of ui
or syntax
for Pulsar to recognize and load it as a theme.Let's create your first theme.
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing Generate Syntax Theme
to generate a new theme package. Select Generate Syntax Theme
, and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Syntax themes should end with -syntax and UI themes should end with -ui.
Pulsar will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Pulsar by pressing LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L to see the changes you made reflected in your Pulsar window. Pretty neat!
Tip
You can avoid reloading to see changes you make by opening an Pulsar window in Dev Mode. To open a Dev Mode Pulsar window run pulsar --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Pulsar's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
',13),C={href:"https://github.com/pulsar-edit/ui-theme-template",target:"_blank",rel:"noopener noreferrer"},P=o("pulsar --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
pulsar -p link --dev
to symlink your repository to LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar
Tip
Because we used pulsar -p link --dev
in the above instructions, if you break anything you can always close Pulsar and launch Pulsar normally to force Pulsar to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
',4),S={href:"https://github.com/pulsar-edit/pulsar/blob/master/static/variables/ui-variables.less",target:"_blank",rel:"noopener noreferrer"},I={href:"https://github.com/pulsar-edit/pulsar/blob/master/static/variables/syntax-variables.less",target:"_blank",rel:"noopener noreferrer"},N=e("p",null,"These default values will be used as a fallback in case a theme doesn't define its own variables.",-1),q=e("h4",{id:"use-in-packages",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#use-in-packages","aria-hidden":"true"},"#"),t(" Use in Packages")],-1),L=e("p",null,[t("In any of your package's "),e("code",null,".less"),t(" files, you can access the theme variables by importing the "),e("code",null,"ui-variables"),t(" or "),e("code",null,"syntax-variables"),t(" file from Pulsar.")],-1),M={href:"https://github.com/pulsar-edit/styleguide",target:"_blank",rel:"noopener noreferrer"},D=o(`Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
To launch a Dev Mode window:
pulsar --dev
If you'd like to reload all the styles at any time, you can use the shortcut LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L,
Pulsar is based on the Chromium browser and supports its Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I
The dev tools allow you to inspect elements and take a look at their CSS properties.
To open the Styleguide, open the command palette with LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or use the shortcut LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes unusable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Pulsar in "normal" mode to fix the issue, it's advised to open two Pulsar windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Pulsar users, it's time to publish it. \u{1F389}
',9);function G(j,B){const a=n("ExternalLinkIcon"),r=n("RouterLink");return u(),p("div",null,[f,e("p",null,[t("Pulsar's interface is rendered using HTML, and it's styled via "),e("a",y,[t("Less"),s(a)]),t(" which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.")]),k,e("ul",null,[e("li",null,[t("Less is a superset of CSS, but it has some really handy features like variables. If you aren't familiar with its syntax, take a few minutes to "),e("a",v,[t("familiarize yourself"),s(a)]),t(".")]),w,e("li",null,[t("You can find existing themes to install or fork in "),e("a",_,[t("Pulsar Package Repository"),s(a)]),t(". "),m("TODO: Update to a themes URL if we get one on the front end site")])]),x,e("ol",null,[e("li",null,[t("Fork the "),e("a",C,[t("ui-theme-template"),s(a)])]),P]),T,e("ul",null,[e("li",null,[e("a",S,[t("ui-variables.less"),s(a)])]),e("li",null,[e("a",I,[t("syntax-variables.less"),s(a)])])]),N,q,L,e("p",null,[t("Your package should generally only specify structural styling, and these should come from "),e("a",M,[t("the style guide"),s(a)]),t(". Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!")]),D,e("p",null,[t("Reloading by pressing "),A,t(": "),R,t(" - "),O,t(": "),V,t(" after you make changes to your theme is less than ideal. Pulsar supports "),e("a",W,[t("live updating"),s(a)]),t(" of styles on Pulsar windows in Dev Mode.")]),U,e("p",null,[t("Check out Google's "),e("a",X,[t("extensive tutorial"),s(a)]),t(" for a short introduction.")]),E,e("p",null,[t("If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The "),e("a",Y,[t("Styleguide"),s(a)]),t(" is a page that renders every component Pulsar supports.")]),F,e("p",null,[t("Follow the steps on the "),s(r,{to:"/docs/launch-manual/sections/core-hacking/sections/#publishing/"},{default:g(()=>[t("Publishing")]),_:1}),t(" page. The example used is for the Word Count package, but publishing a theme works exactly the same.")])])}const K=h(b,[["render",G],["__file","creating-a-theme.html.vue"]]);export{K as default}; diff --git a/assets/creating-a-theme.html.9a1c6a33.js b/assets/creating-a-theme.html.9a1c6a33.js new file mode 100644 index 0000000000..cc82378603 --- /dev/null +++ b/assets/creating-a-theme.html.9a1c6a33.js @@ -0,0 +1,12 @@ +import{_ as r,a as d,b as c}from"./theme-side-by-side.33cf8d5d.js";import{_ as h}from"./dev-tools.1b7b2813.js";import{_ as p,o as u,c as m,a as t,b as e,d as o,w as i,f as s,r as l}from"./app.0e1565ce.js";const g={},f=t("h3",{id:"creating-a-theme",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#creating-a-theme","aria-hidden":"true"},"#"),e(" Creating a Theme")],-1),b={href:"http://lesscss.org/",target:"_blank",rel:"noopener noreferrer"},y=s('Atom supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the Atom > PreferencesFile > PreferencesEdit > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
',5),k={href:"https://speakerdeck.com/danmatthews/less-css",target:"_blank",rel:"noopener noreferrer"},v=t("code",null,"package.json",-1),w=t("code",null,"package.json",-1),_=t("li",null,[e("Your theme's "),t("code",null,"package.json"),e(" must contain a "),t("code",null,"theme"),e(" key with a value of "),t("code",null,"ui"),e(" or "),t("code",null,"syntax"),e(" for Atom to recognize and load it as a theme.")],-1),x={href:"https://atom.io/themes",target:"_blank",rel:"noopener noreferrer"},T=s('Let's create your first theme.
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Syntax Theme" to generate a new theme package. Select "Generate Syntax Theme," and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Tip: Syntax themes should end with -syntax and UI themes should end with -ui.
Atom will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with Cmd+,Ctrl+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Atom by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R to see the changes you made reflected in your Atom window. Pretty neat!
Tip
Tip: You can avoid reloading to see changes you make by opening an Atom window in Dev Mode. To open a Dev Mode Atom window run atom --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
Note: It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Atom's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
',13),A={href:"https://github.com/atom-community/ui-theme-template",target:"_blank",rel:"noopener noreferrer"},C=s('atom --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
apm link --dev
to symlink your repository to ~/.atom/dev/packages
Tip
Tip: Because we used apm link --dev
in the above instructions, if you break anything you can always close Atom and launch Atom normally to force Atom to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
',4),q={href:"https://github.com/atom/atom/blob/master/static/variables/ui-variables.less",target:"_blank",rel:"noopener noreferrer"},D={href:"https://github.com/atom/atom/blob/master/static/variables/syntax-variables.less",target:"_blank",rel:"noopener noreferrer"},I=t("p",null,"These default values will be used as a fallback in case a theme doesn't define its own variables.",-1),M=t("h5",{id:"use-in-packages",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#use-in-packages","aria-hidden":"true"},"#"),e(" Use in Packages")],-1),L=t("p",null,[e("In any of your package's "),t("code",null,".less"),e(" files, you can access the theme variables by importing the "),t("code",null,"ui-variables"),e(" or "),t("code",null,"syntax-variables"),e(" file from Atom.")],-1),R={href:"https://github.com/atom/styleguide",target:"_blank",rel:"noopener noreferrer"},V=s(`Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
To launch a Dev Mode window:
atom --dev
If you'd like to reload all the styles at any time, you can use the shortcut Alt+Cmd+Ctrl+LAlt+Ctrl+R.
Atom is based on the Chrome browser, and supports Chrome's Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the Alt+Cmd+ICtrl+Shift+I shortcut.
The dev tools allow you to inspect elements and take a look at their CSS properties.
To open the Styleguide, open the command palette with Cmd+Shift+PCtrl+Shift+P and search for "styleguide", or use the shortcut Cmd+Ctrl+Shift+GCtrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes un-usable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Atom in "normal" mode to fix the issue, it's advised to open two Atom windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Atom users, it's time to publish it. \u{1F389}
',9);function F(B,z){const a=l("ExternalLinkIcon"),n=l("RouterLink");return u(),m("div",null,[f,t("p",null,[e("Atom's interface is rendered using HTML, and it's styled via "),t("a",b,[e("Less"),o(a)]),e(" which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.")]),y,t("ul",null,[t("li",null,[e("Less is a superset of CSS, but it has some really handy features like variables. If you aren't familiar with its syntax, take a few minutes to "),t("a",k,[e("familiarize yourself"),o(a)]),e(".")]),t("li",null,[e("You may also want to review the concept of a "),v,e(" (as covered in "),o(n,{to:"/hacking-atom/sections/package-word-count/#packagejson"},{default:i(()=>[e("Atom "),w]),_:1}),e("). This file is used to help distribute your theme to Atom users.")]),_,t("li",null,[e("You can find existing themes to install or fork in "),t("a",x,[e("the atom.io themes registry"),o(a)]),e(".")])]),T,t("ol",null,[t("li",null,[e("Fork the "),t("a",A,[e("ui-theme-template"),o(a)])]),C]),S,t("ul",null,[t("li",null,[t("a",q,[e("ui-variables.less"),o(a)])]),t("li",null,[t("a",D,[e("syntax-variables.less"),o(a)])])]),I,M,L,t("p",null,[e("Your package should generally only specify structural styling, and these should come from "),t("a",R,[e("the style guide"),o(a)]),e(". Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!")]),V,t("p",null,[e("Reloading by pressing "),P,O,e(" after you make changes to your theme is less than ideal. Atom supports "),t("a",U,[e("live updating"),o(a)]),e(" of styles on Atom windows in Dev Mode.")]),E,t("p",null,[e("Check out Google's "),t("a",N,[e("extensive tutorial"),o(a)]),e(" for a short introduction.")]),Y,t("p",null,[e("If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The "),t("a",G,[e("Styleguide"),o(a)]),e(" is a page that renders every component Atom supports.")]),j,t("p",null,[e("Follow the steps on the "),o(n,{to:"/docs/atom-archive/hacking-atom/publishing/"},{default:i(()=>[e("Publishing")]),_:1}),e(" page. The example used is for the Word Count package, but publishing a theme works exactly the same.")])])}const K=p(g,[["render",F],["__file","creating-a-theme.html.vue"]]);export{K as default}; diff --git a/assets/creating-a-theme.html.a5fedbd5.js b/assets/creating-a-theme.html.a5fedbd5.js new file mode 100644 index 0000000000..1c8e988460 --- /dev/null +++ b/assets/creating-a-theme.html.a5fedbd5.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-049d4d93","path":"/docs/atom-archive/hacking-atom/sections/creating-a-theme.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Creating a Theme","slug":"creating-a-theme","link":"#creating-a-theme","children":[]}],"git":{"updatedTime":1664050274000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":5.37,"words":1610},"filePathRelative":"docs/atom-archive/hacking-atom/sections/creating-a-theme.md"}');export{e as data}; diff --git a/assets/creating-a-theme.html.e0e52d2e.js b/assets/creating-a-theme.html.e0e52d2e.js new file mode 100644 index 0000000000..aad82d4b6b --- /dev/null +++ b/assets/creating-a-theme.html.e0e52d2e.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-f63d57d8","path":"/docs/launch-manual/sections/core-hacking/sections/creating-a-theme.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Creating a Theme","slug":"creating-a-theme","link":"#creating-a-theme","children":[{"level":3,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[]},{"level":3,"title":"Creating a Syntax Theme","slug":"creating-a-syntax-theme","link":"#creating-a-syntax-theme","children":[]},{"level":3,"title":"Creating a UI Theme","slug":"creating-a-ui-theme","link":"#creating-a-ui-theme","children":[]},{"level":3,"title":"Theme Variables","slug":"theme-variables","link":"#theme-variables","children":[]},{"level":3,"title":"Development workflow","slug":"development-workflow","link":"#development-workflow","children":[]},{"level":3,"title":"Publish your theme","slug":"publish-your-theme","link":"#publish-your-theme","children":[]}]}],"git":{"updatedTime":1670811406000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":5.24,"words":1572},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/creating-a-theme.md"}');export{e as data}; diff --git a/assets/cross-platform-compatibility.html.12555539.js b/assets/cross-platform-compatibility.html.12555539.js new file mode 100644 index 0000000000..a44fd18e47 --- /dev/null +++ b/assets/cross-platform-compatibility.html.12555539.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-0762ec4e","path":"/docs/atom-archive/hacking-atom/sections/cross-platform-compatibility.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Cross-Platform Compatibility","slug":"cross-platform-compatibility","link":"#cross-platform-compatibility","children":[]}],"git":{"updatedTime":1664050274000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":1.81,"words":543},"filePathRelative":"docs/atom-archive/hacking-atom/sections/cross-platform-compatibility.md"}');export{t as data}; diff --git a/assets/cross-platform-compatibility.html.1dd2761e.js b/assets/cross-platform-compatibility.html.1dd2761e.js new file mode 100644 index 0000000000..e0356696be --- /dev/null +++ b/assets/cross-platform-compatibility.html.1dd2761e.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-78bfd6da","path":"/docs/launch-manual/sections/core-hacking/sections/cross-platform-compatibility.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Cross-Platform Compatibility","slug":"cross-platform-compatibility","link":"#cross-platform-compatibility","children":[]}],"git":{"updatedTime":1670466847000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":1.81,"words":543},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/cross-platform-compatibility.md"}');export{t as data}; diff --git a/assets/cross-platform-compatibility.html.ce60d4e3.js b/assets/cross-platform-compatibility.html.ce60d4e3.js new file mode 100644 index 0000000000..d30076d4d2 --- /dev/null +++ b/assets/cross-platform-compatibility.html.ce60d4e3.js @@ -0,0 +1,4 @@ +import{_ as i,o as s,c as r,a,b as e,d as n,f as t,r as d}from"./app.0e1565ce.js";const l={},c=t('Atom runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)"c:\\my test"
/my\\ test
\\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\\
and d:\\
CRLF
LF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
Pulsar runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)/my\\ test
"c:\\my test"
\\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\\
and d:\\
LF
CRLF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
You might be running into an issue which was already fixed in a more recent version of Pulsar than the one you're using.
If you're using a released version, check which version of Pulsar you're using:
$ pulsar --version
+> Pulsar : 1.63.0-dev
+> Electron: 12.2.3
+> Chrome : 89.0.4389.128
+> Node : 14.16.0
+
If you're building Pulsar from source, pull down the latest version of master and re-build. Make sure that if logging an issue you include the latest commit hash you built from.
A large part of Pulsar's functionality comes from packages you can install. Pulsar will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Pulsar from the terminal in safe mode:
$ pulsar --safe
+
This starts Pulsar, but does not load packages from LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\packages
or %USERPROFILE%\\.pulsar\\dev\\packages
. and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Pulsar normally still produces the error, then try figuring out which package is causing trouble. Start Pulsar normally again and open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Pulsar or reload Pulsar with LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L. after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Pulsar saves a number of things about your environment when you exit in order to restore Pulsar to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Pulsar from working properly. In these cases, you may want to clear the state that Pulsar has saved.
DANGER
Clearing the saved state permanently destroys any state that Pulsar has saved across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ pulsar --clear-window-state
+
In some cases, you may want to reset Pulsar to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
`,15),L=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[t("$ "),e("span",{class:"token function"},"mv"),t(` ~/.pulsar ~/.pulsar-backup +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),R=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[t("$ "),e("span",{class:"token function"},"mv"),t(` ~/.pulsar ~/.pulsar-backup +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),W=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[t("$ "),e("span",{class:"token function"},"rename"),t(" %USERPROFILE%"),e("span",{class:"token punctuation"},"\\"),t(`.pulsar .pulsar-backup +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),E=o(`Once that is complete, you can launch Pulsar as normal. Everything will be just as if you first installed Pulsar.
Tip
The command given above doesn't delete the old configuration, just puts it somewhere that Pulsar can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the LNX/MAC: ~/.pulsar-backup
- WIN: %USERPROFILE%\\.pulsar-backup
directory.
If you develop or contribute to Pulsar packages, there may be left-over packages linked to your LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\packages
or %USERPROFILE%\\.pulsar\\dev\\packages
. directories. You can use the pulsar -p links
command to list all linked packages:
$ pulsar -p links
+> /Users/pulsy/.pulsar/dev/packages (0)
+> \u2514\u2500\u2500 (no links)
+> /Users/pulsy/.pulsar/packages (1)
+> \u2514\u2500\u2500 color-picker -> /Users/pulsy/github/color-picker
+
You can remove links using the pulsar -p unlink
command:
$ pulsar -p unlink color-picker
+> Unlinking /Users/pulsy/.pulsar/packages/color-picker \u2713
+
See pulsar -p links --help
and pulsar -p unlink --help
for more information on these commands.
Tip
You can also use pulsar -p unlink --all
to easily unlink all packages and themes.
Show the keybinding resolver with LNX/WIN: Ctrl+. - MAC: Cmd+., or with Keybinding Resolver: Show
from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
',5),ce=e("ul",null,[e("li",null,[e("p",null,"The key combination was not used in the context defined by the keybinding's selector"),e("p",null,[t("For example, you can't trigger the keybinding for the "),e("code",null,"tree-view:add-file"),t(" command if the Tree View is not focused.")])]),e("li",null,[e("p",null,"There is another keybinding that took precedence"),e("p",null,"This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.")])],-1),de=e("code",null,"keymap.cson",-1),ue={href:"https://github.com/orgs/pulsar-edit/discussions",target:"_blank",rel:"noopener noreferrer"},he=e("h3",{id:"check-font-rendering-issues",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#check-font-rendering-issues","aria-hidden":"true"},"#"),t(" Check Font Rendering Issues")],-1),pe=e("strong",null,[e("em",null,"LNX/WIN")],-1),ge=e("kbd",null,"Ctrl+Shift+I",-1),me=e("strong",null,[e("em",null,"MAC")],-1),be=e("kbd",null,"Alt+Cmd+I",-1),fe={href:"https://developers.google.com/web/tools/chrome-devtools/inspect-styles/",target:"_blank",rel:"noopener noreferrer"},ke=o('When an unexpected error occurs in Pulsar, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
To run a profile, open the Developer Tools with LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Pulsar:
$ pulsar --profile-startup .
+
This will automatically capture a CPU profile as Pulsar is loading and open the Developer Tools once Pulsar loads. From there:
You can then include the startup profile in any issue you report.
If you are having issues installing a package using pulsar -p install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run pulsar -p install --check
to see if the Pulsar package manager can build native code on your machine.
Check out the pre-requisites in the build instructions for your platform for more details.
If you encounter flickering or other rendering issues, you can stop Pulsar from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ pulsar --disable-gpu
+
Chromium (and thus Pulsar) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Pulsar to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage on portable devices as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ pulsar --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Pulsar flags as they will not be executed after the Chromium flags e.g.:
$ pulsar --safe --enable-gpu-rasterization --force-gpu-rasterization
+
You might be running into an issue which was already fixed in a more recent version of Atom than the one you're using.
If you're using a released version, check which version of Atom you're using:
$ atom --version
+> Atom : 1.8.0
+> Electron: 0.36.8
+> Chrome : 47.0.2526.110
+> Node : 5.1.1
+
A large part of Atom's functionality comes from packages you can install. Atom will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Atom from the terminal in safe mode:
$ atom --safe
+
This starts Atom, but does not load packages from ~/.atom/packages
or ~/.atom/dev/packages
and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Atom normally still produces the error, then try figuring out which package is causing trouble. Start Atom normally again and open the Settings View with Cmd+,Ctrl+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Atom or reload Atom with Alt+Cmd+Ctrl+LCtrl+Shift+F5 after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Atom saves a number of things about your environment when you exit in order to restore Atom to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Atom from working properly. In these cases, you may want to clear the state that Atom has saved.
DANGER
:rotatinglight: Danger: Clearing the saved state permanently destroys any state that Atom has saved _across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ atom --clear-window-state
+
In some cases, you may want to reset Atom to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
`,14),L=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ mv ~/.atom ~/.atom-backup +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),N=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ mv ~/.atom ~/.atom-backup +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),O=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ rename %USERPROFILE%\\.atom .atom-backup +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),V=l(`Once that is complete, you can launch Atom as normal. Everything will be just as if you first installed Atom.
Tip
Tip: The command given above doesn't delete the old configuration, just puts it somewhere that Atom can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the ~/.atom-backup
%USERPROFILE%\\.atom-backup
directory.
If you develop or contribute to Atom packages, there may be left-over packages linked to your ~/.atom/packages
or ~/.atom/dev/packages
directories. You can use the apm links
command to list all linked packages:
$ apm links
+> /Users/octocat/.atom/dev/packages (0)
+> \u2514\u2500\u2500 (no links)
+> /Users/octocat/.atom/packages (1)
+> \u2514\u2500\u2500 color-picker -> /Users/octocat/github/color-picker
+
You can remove links using the apm unlink
command:
$ apm unlink color-picker
+> Unlinking /Users/octocat/.atom/packages/color-picker \u2713
+
See apm links --help
and apm unlink --help
for more information on these commands.
Tip
Tip: You can also use apm unlink --all
to easily unlink all packages and themes.
Show the keybinding resolver with Cmd+.Ctrl+. or with "Keybinding Resolver: Show" from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
',5),ce=e("ul",null,[e("li",null,[e("p",null,"The key combination was not used in the context defined by the keybinding's selector"),e("p",null,[t("For example, you can't trigger the keybinding for the "),e("code",null,"tree-view:add-file"),t(" command if the Tree View is not focused.")])]),e("li",null,[e("p",null,"There is another keybinding that took precedence"),e("p",null,"This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.")])],-1),de=e("code",null,"keymap.cson",-1),he={href:"https://github.com/atom/atom/discussions",target:"_blank",rel:"noopener noreferrer"},ue=e("h4",{id:"check-font-rendering-issues",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#check-font-rendering-issues","aria-hidden":"true"},"#"),t(" Check Font Rendering Issues")],-1),me=e("kbd",{class:"platform-mac"},"Alt+Cmd+I",-1),pe=e("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+I",-1),ge={href:"https://developers.google.com/web/tools/chrome-devtools/inspect-styles/",target:"_blank",rel:"noopener noreferrer"},fe=l('When an unexpected error occurs in Atom, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press Alt-Cmd-ICtrl-Shift-I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
Note: When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
To run a profile, open the Developer Tools with Alt+Cmd+ICtrl+Shift+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Atom:
$ atom --profile-startup .
+
This will automatically capture a CPU profile as Atom is loading and open the Developer Tools once Atom loads. From there:
You can then include the startup profile in any Issue you report.
If you are having issues installing a package using apm install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run apm install --check
to see if the Atom package manager can build native code on your machine.
If you encounter flickering or other rendering issues, you can stop Atom from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ atom --disable-gpu
+
Chromium (and thus Atom) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Atom to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ atom --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Atom flags as they will not be executed after the Chromium flags e.g.:
$ atom --safe --enable-gpu-rasterization --force-gpu-rasterization
+
Atom contains a number of packages that are Node modules instead of Atom packages. If you want to make changes to the Node modules, for instance atom-keymap
, you have to link them into the development environment differently than you would a normal Atom package.
Here are the steps to run a local version of a Node module within Atom. We're using atom-keymap
as an example:
::: tab#developing-node-modules
$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than ~/github/atom
+# you need to set the following environment variable
+$ export ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than %USERPROFILE%\\github\\atom
+# you need to set the following environment variable
+$ setx ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
:::
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Atom and do the following:
$ cd <em>WHERE YOU CLONED THE NODE MODULE</em>
+$ npm install
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ apm rebuild
+$ atom --dev .
+
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Pulsar and do the following:
$ cd <WHERE YOU CLONED THE NODE MODULE>
+$ npm install
+$ cd <WHERE YOU CLONED PULSAR>
+$ pulsar -p rebuild
+$ pulsar --dev .
+
The documentation for the project should maintain a consistent style where possible. This covers a number of aspects such as writing style, naming conventions, folder structure, links etc.
All docs are currently in American English (en-US) but localization is planned.
The main structure for documentation can be seen in docs/docs
. There are a number of "root" sections:
launch-manual
- For the current main Pulsar documentationpackages
- Currently holds wiki info from the atom package reposresources
- For other referenced docsblog
- For the website blogatom-archive
- For "as is" archived Atom documentationWithin each section is an index.md
which will usually contain info about each sub-section as well as links to them. These correspond to the second level items on the sidebar.
Inside sections
are the sub-sections which group more specific topics. These also have an index.md
which corresponds to the third level item on the sidebar. This file is displayed on the website as a single long documenent but is actually created from a number of @include() lines which reference individual sections within the next sections directory. These should be relative to the location of index.md e.g. @include(sections/pulsar-packages.md). This file also contains the frontmatter for defining the title, language and description of the file and should also be the first level heading for the page. Here is also where you can place a container such as Under Construction
to apply to the entire page.
Inside the next sections
directory should be the actual content of the document. Each section should start with a second level header and should not contain any frontmatter.
Internal links can just be to the header (e.g.[Structure](#structure)
), this to all sections included on the parent index.md
so care should be made to not create any duplicate headers.
All other links should be relative but do not need to reference the index file itself (e.g.[Installing](../getting-started#installing)
) will find the heading #installing
within the index file in the getting-started
directory above.
Images should be added to [pulsar-assets](https://github.com/pulsar-edit/pulsar-assets)
and referenced from the package imported from it. This is done via an alias on the .vuepress/config.js
file which adds most of the path for you: '@images': path.resolve(__dirname, '../../node_modules/pulsar-assets/images')
so the link to your image would just be ![myImage](@images/pulsar/myImage.png "My Image")
.
The name of the application is Pulsar
and should be capitalized as such. Whilst the website and GitHub org name is Pulsar-Edit
, this should not be used within documentation outside of links to the GitHub org or website.
Operating systems should be named as such:
Linux
- All GNU/Linux distributionsmacOS
- Apple's current operating system familyWindows
- Microsoft Windows family of operating systemsThis is also the order they should appear in within the tab switcher.
When using the #tabs
switcher they should be in this order.
When referencing them inline then they should be abbreviated to the following, strongly emphasized and separated by a -
:
To keep order consistent it should be LNX -> MAC -> WIN. If instructions: common to two then it should either be LNX/MAC, LNX/WIN -> MAC/WIN
For Linux we may sometimes need to reference particular distros or families of distributions. We currently use:
Ubuntu/Debian
for all distributions based on Debian or UbuntuFedora/RHEL
for all distrububtions based on Fedora Linux & Red Hat Red Hat Enterprise Linux. This includes AlmaLinux, CentOS, Rocky Linux etc.Arch
- for all Arch based distributions such as Manjaro, Garuda, ArcoLinux etc.OpenSUSE
- for all OpenSUSE based distributions such as GeckoLinuxWe may need to add more in the future but generally users of less popular or more technical distributions such as Gentoo or NixOS understand how to adapt to their OS from the instructions above.
Where you want to display an info
, warning
or tab/code switcher in the document you should use a container with the :::
syntax.
e.g.
::: tabs#filename
+
+@tab Linux
+
+Lorem ipsum dolor sit amet...
+
+@tab macOS
+
+Lorem ipsum dolor sit amet...
+
+@tab Windows
+
+Lorem ipsum dolor sit amet...
+
+:::
+
or
::: tip My Helpful Tip
+
+You might want to do X to get Y
+
+:::
+
"Rolling" and "Regular" Releases
We have "Rolling" and "Regular" releases to choose from. These differ only in the frequency of updates they receive.
Note: There are no automatic updates at this time, regardless of which type of download you choose.
So far we've looked at a number of ways to move around and select regions of a file, so now let's actually change some of that text. Obviously you can type in order to insert characters, but there are also a number of ways to delete and manipulate text that could come in handy.
There are a handful of cool keybindings for basic text manipulation that might come in handy. These range from moving around lines of text and duplicating lines to changing the case.
Atom also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using Alt+Cmd+QAlt+Ctrl+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
One of the cool things that Atom can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.
Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any plugin or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the CmdCtrl key pressed down to select multiple regions of your text simultaneously.
Atom comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
',6),A={href:"https://github.com/atom/whitespace",target:"_blank",rel:"noopener noreferrer"},T=e("code",null,"whitespace",-1),D=o('Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Atom, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Atom will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Atom ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Atom will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, \u201C\u201D
, \u2018\u2019
, \xAB\xBB
, \u2039\u203A
, and backticks when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Atom will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
Atom also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.
If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.
When you open a file, Atom will try to auto-detect the encoding. If Atom can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.
If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.
',7),L={href:"https://github.com/atom/encoding-selector",target:"_blank",rel:"noopener noreferrer"};function M(I,B){const l=d("Tabs"),n=d("ExternalLinkIcon");return f(),b("div",null,[k,a(l,{id:"39",data:[{title:"Mac"}],"tab-id":"editing-and-deleting"},{tab0:i(({title:s,value:r,isActive:c})=>[w]),_:1}),y,a(l,{id:"76",data:[{title:"Mac"}],"tab-id":"editing-and-deleting"},{tab0:i(({title:s,value:r,isActive:c})=>[x]),_:1}),_,a(l,{id:"125",data:[{title:"Mac"}],"tab-id":"editing-and-deleting"},{tab0:i(({title:s,value:r,isActive:c})=>[C]),_:1}),v,e("p",null,[t("The whitespace commands are implemented in the "),e("a",A,[t("atom/whitespace"),a(n)]),t(" package. The settings for the whitespace commands are managed on the page for the "),T,t(" package.")]),D,e("p",null,[t("The brackets functionality is implemented in the "),e("a",S,[t("bracket-matcher"),a(n)]),t(" package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.")]),U,e("p",null,[t("The encoding selector is implemented in the "),e("a",L,[t("encoding-selector"),a(n)]),t(" package.")])])}const E=u(g,[["render",M],["__file","editing-and-deleting-text.html.vue"]]);export{E as default}; diff --git a/assets/editing-and-deleting-text.html.31c0ca86.js b/assets/editing-and-deleting-text.html.31c0ca86.js new file mode 100644 index 0000000000..faa7f5da29 --- /dev/null +++ b/assets/editing-and-deleting-text.html.31c0ca86.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-73d97d01","path":"/docs/launch-manual/sections/using-pulsar/sections/editing-and-deleting-text.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Editing and Deleting Text","slug":"editing-and-deleting-text","link":"#editing-and-deleting-text","children":[{"level":3,"title":"Basic Manipulation","slug":"basic-manipulation","link":"#basic-manipulation","children":[]},{"level":3,"title":"Deleting and Cutting","slug":"deleting-and-cutting","link":"#deleting-and-cutting","children":[]},{"level":3,"title":"Multiple Cursors and Selections","slug":"multiple-cursors-and-selections","link":"#multiple-cursors-and-selections","children":[]},{"level":3,"title":"Whitespace","slug":"whitespace","link":"#whitespace","children":[]},{"level":3,"title":"Brackets","slug":"brackets","link":"#brackets","children":[]},{"level":3,"title":"Encoding","slug":"encoding","link":"#encoding","children":[]}]}],"git":{"updatedTime":1669229414000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":5}]},"readingTime":{"minutes":5.24,"words":1571},"filePathRelative":"docs/launch-manual/sections/using-pulsar/sections/editing-and-deleting-text.md"}');export{e as data}; diff --git a/assets/editing-and-deleting-text.html.77f55668.js b/assets/editing-and-deleting-text.html.77f55668.js new file mode 100644 index 0000000000..e7f4c01b00 --- /dev/null +++ b/assets/editing-and-deleting-text.html.77f55668.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-34299b28","path":"/docs/atom-archive/using-atom/sections/editing-and-deleting-text.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Editing and Deleting Text","slug":"editing-and-deleting-text","link":"#editing-and-deleting-text","children":[]}],"git":{"updatedTime":1664634443000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":4.5,"words":1349},"filePathRelative":"docs/atom-archive/using-atom/sections/editing-and-deleting-text.md"}');export{e as data}; diff --git a/assets/editing-and-deleting-text.html.cf992d5a.js b/assets/editing-and-deleting-text.html.cf992d5a.js new file mode 100644 index 0000000000..f7d6520938 --- /dev/null +++ b/assets/editing-and-deleting-text.html.cf992d5a.js @@ -0,0 +1 @@ +import{b as u,_ as h,a as p}from"./encodings.f93acf64.js";import{_ as b,o as m,c as g,d as a,w as l,a as e,b as t,f as c,r as d}from"./app.0e1565ce.js";const k={},f=e("h2",{id:"editing-and-deleting-text",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#editing-and-deleting-text","aria-hidden":"true"},"#"),t(" Editing and Deleting Text")],-1),w=e("p",null,"So far we've looked at a number of ways to move around and select regions of a file, so now let's actually change some of that text. Obviously you can type in order to insert characters, but there are also a number of ways to delete and manipulate text that could come in handy.",-1),_=e("h3",{id:"basic-manipulation",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#basic-manipulation","aria-hidden":"true"},"#"),t(" Basic Manipulation")],-1),C=e("p",null,"There are a handful of cool keybindings for basic text manipulation that might come in handy. These range from moving around lines of text and duplicating lines to changing the case.",-1),y=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+J"),t(" - Join the next line to the end of the current line")]),e("li",null,[e("kbd",null,"Ctrl+Up/Down"),t(" - Move the current line up or down")]),e("li",null,[e("kbd",null,"Ctrl+Shift+D"),t(" - Duplicate the current line")]),e("li",null,[e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+U"),t(" - Upper case the current word")]),e("li",null,[e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+L"),t(" - Lower case the current word")])],-1),v=e("ul",null,[e("li",null,[e("kbd",null,"Cmd+J"),t(" - Join the next line to the end of the current line")]),e("li",null,[e("kbd",null,"Cmd+Ctrl+Up/Down"),t(" - Move the current line up or down")]),e("li",null,[e("kbd",null,"Cmd+Shift+D"),t(" - Duplicate the current line")]),e("li",null,[e("kbd",null,"Cmd+K"),t(),e("kbd",null,"Cmd+U"),t(" - Upper case the current word")]),e("li",null,[e("kbd",null,"Cmd+K"),t(),e("kbd",null,"Cmd+L"),t(" - Lower case the current word")]),e("li",null,[e("kbd",null,"Ctrl+T"),t(" - Transpose characters. This swaps the two characters on either side of the cursor.")])],-1),x=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+J"),t(" - Join the next line to the end of the current line")]),e("li",null,[e("kbd",null,"Ctrl+Up/Down"),t(" - Move the current line up or down")]),e("li",null,[e("kbd",null,"Ctrl+Shift+D"),t(" - Duplicate the current line")]),e("li",null,[e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+U"),t(" - Upper case the current word")]),e("li",null,[e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+L"),t(" - Lower case the current word")])],-1),A=c('Pulsar also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using LNX/WIN: Ctrl+Shift+Q - MAC: Alt+Cmd+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
',3),D=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Shift+K"),t(" - Delete current line")]),e("li",null,[e("kbd",null,"Ctrl+Backspace"),t(" - Delete to beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Delete"),t(" - Delete to end of word")])],-1),S=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Shift+K"),t(" - Delete current line")]),e("li",null,[e("kbd",null,"Alt+Backspace"),t(" or "),e("kbd",null,"Alt+H"),t(" - Delete to beginning of word")]),e("li",null,[e("kbd",null,"Alt+Delete"),t(" or "),e("kbd",null,"Alt+D"),t(" - Delete to end of word")]),e("li",null,[e("kbd",null,"Cmd+Delete"),t(" - Delete to end of line")]),e("li",null,[e("kbd",null,"Ctrl+K"),t(" - Cut to end of line")]),e("li",null,[e("kbd",null,"Cmd+Backspace"),t(" - Delete to beginning of line")])],-1),T=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Shift+K"),t(" - Delete current line")]),e("li",null,[e("kbd",null,"Ctrl+Backspace"),t(" - Delete to beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Delete"),t(" - Delete to end of word")])],-1),L=e("h3",{id:"multiple-cursors-and-selections",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#multiple-cursors-and-selections","aria-hidden":"true"},"#"),t(" Multiple Cursors and Selections")],-1),M=e("p",null,"One of the cool things that Pulsar can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.",-1),U=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Click"),t(" - Add a new cursor at the clicked location")]),e("li",null,[e("kbd",null,"Alt+Shift+Up/Down"),t(" - Add another cursor above/below the current cursor")]),e("li",null,[e("kbd",null,"Ctrl+D"),t(" - Select the next word in the document that is the same as the currently selected word")]),e("li",null,[e("kbd",null,"Alt+F3"),t(" - Select all words in the document that are the same as the currently selected word")])],-1),I=e("ul",null,[e("li",null,[e("kbd",null,"Cmd+Click"),t(" - Add a new cursor at the clicked location")]),e("li",null,[e("kbd",null,"Ctrl+Shift+Up/Down"),t(" - Add another cursor above/below the current cursor")]),e("li",null,[e("kbd",null,"Cmd+D"),t(" - Select the next word in the document that is the same as the currently selected word")]),e("li",null,[e("kbd",null,"Cmd+Ctrl+G"),t(" - Select all words in the document that are the same as the currently selected word")]),e("li",null,[e("kbd",null,"Cmd+Shift+L"),t(" - Convert a multi-line selection into multiple cursors")])],-1),P=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Click"),t(" - Add a new cursor at the clicked location")]),e("li",null,[e("kbd",null,"Alt+Ctrl+Up/Down"),t(" - Add another cursor above/below the current cursor")]),e("li",null,[e("kbd",null,"Ctrl+D"),t(" - Select the next word in the document that is the same as the currently selected word")]),e("li",null,[e("kbd",null,"Alt+F3"),t(" - Select all words in the document that are the same as the currently selected word")])],-1),B=c('Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any package or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the LNX/WIN: Ctrl - MAC: Cmd key pressed down to select multiple regions of your text simultaneously.
Return to a single cursor with Esc or by clicking anywhere in the file to position a cursor there as normal.
Pulsar comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
',7),K={href:"https://github.com/pulsar-edit/whitespace",target:"_blank",rel:"noopener noreferrer"},W=e("code",null,"whitespace",-1),j=c('Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Pulsar, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Pulsar will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Pulsar ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Pulsar will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, \u201C\u201D
, \u2018\u2019
, \xAB\xBB
, \u2039\u203A
, and ``
when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Pulsar will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
',8),J=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+M"),t(" - Jump to the bracket matching the one adjacent to the cursor. It jumps to the nearest enclosing bracket when there's no adjacent bracket.")]),e("li",null,[e("kbd",null,"Alt+Ctrl+,"),t(" - Select all the text inside the current brackets")]),e("li",null,[e("kbd",null,"Alt+Ctrl+."),t(" - Close the current XML/HTML tag")])],-1),N=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+M"),t(" - Jump to the bracket matching the one adjacent to the cursor. It jumps to the nearest enclosing bracket when there's no adjacent bracket.")]),e("li",null,[e("kbd",null,"Cmd+Ctrl+M"),t(" - Select all the text inside the current brackets")]),e("li",null,[e("kbd",null,"Alt+Cmd+."),t(" - Close the current XML/HTML tag")])],-1),q=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+M"),t(" - Jump to the bracket matching the one adjacent to the cursor. It jumps to the nearest enclosing bracket when there's no adjacent bracket.")]),e("li",null,[e("kbd",null,"Alt+Ctrl+,"),t(" - Select all the text inside the current brackets")]),e("li",null,[e("kbd",null,"Alt+Ctrl+."),t(" - Close the current XML/HTML tag")])],-1),O={href:"https://github.com/pulsar-edit/bracket-matcher",target:"_blank",rel:"noopener noreferrer"},E=e("h3",{id:"encoding",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#encoding","aria-hidden":"true"},"#"),t(" Encoding")],-1),X=e("p",null,"Pulsar also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.",-1),H=e("p",null,[e("kbd",null,"Alt+U"),t(" - Toggle menu to change file encoding")],-1),Y=e("p",null,[e("kbd",null,"Ctrl+Shift+U"),t(" - Toggle menu to change file encoding")],-1),F=e("p",null,[e("kbd",null,"Ctrl+Shift+U"),t(" - Toggle menu to change file encoding")],-1),V=e("p",null,"If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.",-1),Q=e("p",null,"When you open a file, Pulsar will try to auto-detect the encoding. If Pulsar can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.",-1),R=e("p",null,[e("img",{src:u,alt:"Changing your file encoding"})],-1),z=e("p",null,"If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.",-1),G={href:"https://github.com/pulsar-edit/encoding-selector",target:"_blank",rel:"noopener noreferrer"};function Z($,ee){const r=d("Tabs"),s=d("ExternalLinkIcon");return m(),g("div",null,[f,w,_,C,a(r,{id:"12",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:l(({title:n,value:i,isActive:o})=>[y]),tab1:l(({title:n,value:i,isActive:o})=>[v]),tab2:l(({title:n,value:i,isActive:o})=>[x]),_:1}),A,a(r,{id:"115",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:l(({title:n,value:i,isActive:o})=>[D]),tab1:l(({title:n,value:i,isActive:o})=>[S]),tab2:l(({title:n,value:i,isActive:o})=>[T]),_:1}),L,M,a(r,{id:"195",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:l(({title:n,value:i,isActive:o})=>[U]),tab1:l(({title:n,value:i,isActive:o})=>[I]),tab2:l(({title:n,value:i,isActive:o})=>[P]),_:1}),B,e("p",null,[t("The whitespace commands are implemented in the "),e("a",K,[t("whitespace"),a(s)]),t(" package. The settings for the whitespace commands are managed on the page for the "),W,t(" package.")]),j,a(r,{id:"324",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:l(({title:n,value:i,isActive:o})=>[J]),tab1:l(({title:n,value:i,isActive:o})=>[N]),tab2:l(({title:n,value:i,isActive:o})=>[q]),_:1}),e("p",null,[t("The brackets functionality is implemented in the "),e("a",O,[t("bracket-matcher"),a(s)]),t(" package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.")]),E,X,a(r,{id:"392",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:l(({title:n,value:i,isActive:o})=>[H]),tab1:l(({title:n,value:i,isActive:o})=>[Y]),tab2:l(({title:n,value:i,isActive:o})=>[F]),_:1}),V,Q,R,z,e("p",null,[t("The encoding selector is implemented in the "),e("a",G,[t("encoding-selector"),a(s)]),t(" package.")])])}const ne=b(k,[["render",Z],["__file","editing-and-deleting-text.html.vue"]]);export{ne as default}; diff --git a/assets/electron.0f83d84c.png b/assets/electron.0f83d84c.png new file mode 100644 index 0000000000..7c4e2574b1 Binary files /dev/null and b/assets/electron.0f83d84c.png differ diff --git a/assets/electron.b5867f08.js b/assets/electron.b5867f08.js new file mode 100644 index 0000000000..46698a1fab --- /dev/null +++ b/assets/electron.b5867f08.js @@ -0,0 +1 @@ +const s="/assets/electron.0f83d84c.png";export{s as _}; diff --git a/assets/encodings.782197c2.png b/assets/encodings.782197c2.png new file mode 100644 index 0000000000..40e5bb510a Binary files /dev/null and b/assets/encodings.782197c2.png differ diff --git a/assets/encodings.f93acf64.js b/assets/encodings.f93acf64.js new file mode 100644 index 0000000000..97e74a7af6 --- /dev/null +++ b/assets/encodings.f93acf64.js @@ -0,0 +1 @@ +const s="/assets/multiple-cursors.4466b041.gif",t="/assets/whitespace.59413c09.png",o="/assets/encodings.782197c2.png";export{s as _,t as a,o as b}; diff --git a/assets/exception-notification.f8f467e7.png b/assets/exception-notification.f8f467e7.png new file mode 100644 index 0000000000..5d3b45b847 Binary files /dev/null and b/assets/exception-notification.f8f467e7.png differ diff --git a/assets/file-icons.45aa1c32.png b/assets/file-icons.45aa1c32.png new file mode 100644 index 0000000000..2acf4947c8 Binary files /dev/null and b/assets/file-icons.45aa1c32.png differ diff --git a/assets/file-organization.html.750afb1d.js b/assets/file-organization.html.750afb1d.js new file mode 100644 index 0000000000..80cc72243d --- /dev/null +++ b/assets/file-organization.html.750afb1d.js @@ -0,0 +1,72 @@ +import{_ as t,o,c as d,a as n,b as e,d as a,f as s,r as l}from"./app.0e1565ce.js";const c={},r=s(`One of the most important things to take note of when adding new documentation is where it should go within the website layout.
The generalized overall layout of the website looks like this:
docs
+\u251C\u2500\u2500 root level .md files
+\u2514\u2500\u2500 section folder
+ \u251C\u2500\u2500 sections
+ \u2514\u2500\u2500 index.md
+
The general idea is that for files that can stand by themselves (for example the About Us
page, Repositories
etc.) they exist at the docs/
"root" level.
For anything that is more complex it needs to have a section directory named appropriately, an index.md
file within it and a sections
directory.
index.md
This index file needs to have a YAML frontmatter to define, at a minimum, the title of the document. This is displayed as an H1
header for the page (note: subsequent H1
headers will be ignored so always start at H2
).
The rest of this index file will be used to display the actual content you want to show. This is done in a number of ways.
First of all you can just include standard markdown. This is often used for introducing the section or adding one of our reusable components (e.g. a danger
container).
The rest of the file should consist of @includes
which take data from other folders on the website and integrates it automatically. Usually this will be the sections
files which will be covered next.
e.g.
+File not found
+
+
This is done by having a value defined on the config.js
file which will provide an alias for us to use:
if (file.startsWith("@orgdocs")) {
+ return file.replace(
+ "@orgdocs",
+ path.resolve(__dirname, "../../node_modules/.github/")
+ );
+}
+
This allows us to include org-level docs by using this special alias.
e.g.
@include-push(/home/runner/work/pulsar-edit.github.io/pulsar-edit.github.io/node_modules/.github)
+# Pulsar tooling
+
+Here you will find a list of tools used by the Pulsar team and information about them.
+
+## Continuous Integration
+
+### [Cirrus CI](https://cirrus-ci.com/github/pulsar-edit/pulsar)
+
+Cirrus CI is used for Pulsar's continuous integration as well as for building application binaries.
+
+### [Codacy](https://app.codacy.com/gh/pulsar-edit/repositories)
+
+Codacy is used to scan committed code for any issues that may have been missed.
+
+Currently though Codacy is only used on the following repositories:
+
+* [ppm](https://app.codacy.com/gh/pulsar-edit/ppm/dashboard)
+* [pulsar](https://app.codacy.com/gh/pulsar-edit/pulsar/dashboard)
+* [background-tips](https://app.codacy.com/gh/pulsar-edit/background-tips/dashboard)
+* [autocomplete-plus](https://app.codacy.com/gh/pulsar-edit/autocomplete-plus/dashboard)
+
+## i18n (Internationalization)
+
+### [Crowdin](https://crowdin.pulsar-edit.dev/)
+
+Crowdin will be used for Pulsar's internationalization efforts but exact details on this are still pending.
+
+## Package Managers
+
+While most repositories you can easily tell what Package Manager is being used by checking for a specific lock file, there are some execptions to this that should be noted.
+
+* [pulsar](https://github.com/pulsar-edit/pulsar) uses \`yarn\` as its Package Manager.
+* [pulsar-edit.github.io](https://github.com/pulsar-edit/pulsar-edit.github.io) uses \`pnpm\` as its Package Manager.
+
+## Cloud Database
+
+The [package-backend](https://github.com/pulsar-edit/package-backend) currently uses DigitalOcean to host the PostgreSQL Pulsar Package Repositories data in the cloud.
+
+## Cloud Compute
+
+Both the [package-backend](https://github.com/pulsar-edit/package-backend) and [package-frontend](https://github.com/pulsar-edit/package-frontend) use Google App Engine to host the compute instance of these websites in the cloud.
+
+## Additional Testing tools
+
+### [Action Pulsar Dependency Tester](https://github.com/marketplace/actions/action-pulsar-dependency-tester)
+
+Pulsar Dependency Tester is a GitHub action used to test changes of a core Pulsar Dependency if you need to determine how your core package dependency will run on the current version of the Pulsar Editor.
+
+This GitHub Action is a direct Pulsar replacement of the previous Action to test Atom Dependencies [Setup Atom](https://github.com/marketplace/actions/setup-atom).
+
+@include-pop()
+
+
The sections
directory is where we include the rest of the documents broken down by section. These should be self contained files which can be used alone but are designed to be included on the section page. This approach allows us flexibility with ordering as well as including these files in other places without needing to duplicate the material.
Files here can be navigated to directly on the website but should not be linked to directly.
These files shoud not have any YAML frontmatter as they will be included and shown as text.
An alias for this exists in config.js
to access files from this repository.
alias: {
+ '@images': path.resolve(__dirname, '../../node_modules/.github/images')
+},
+
So to include an image you simply need to use the standard markdown image link along with the alias:
e.g.
![MyImage](@images/path/to/image.png)
+
If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press LNX/WIN: Cmd+F - MAC: Ctrl+F, type in a search string and press LNX/WIN/MAC: Enter or LNX/WINF3 - MAC: Cmd+G or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurrences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Atom" with the string "Pulsar", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
',4),F={class:"custom-container note"},v=e("p",{class:"custom-container-title"},"Note",-1),C=e("p",null,[e("strong",null,"Note:"),t(" Pulsar uses JavaScript regular expressions to perform regular expression searches.")],-1),S={href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions",target:"_blank",rel:"noopener noreferrer"},N=c('You can also find and replace throughout your entire project if you invoke the panel with LNX/WIN: Ctrl+Shift+F - MAC: Cmd+Shift+F.
This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.
',3),j={href:"https://en.wikipedia.org/wiki/Glob_%28programming%29",target:"_blank",rel:"noopener noreferrer"},A=e("code",null,"src/*.js",-1),q=e("code",null,"src",-1),R=e("code",null,"**",-1),I=e("code",null,"docs/**/*.md",-1),T=e("code",null,"docs/a/foo.md",-1),W=e("code",null,"docs/a/b/foo.md",-1),E=e("p",null,[t("When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders "),e("code",null,"/path1/folder1"),t(" and "),e("code",null,"/path2/folder2"),t(" open, you could enter a pattern starting with "),e("code",null,"folder1"),t(" to search only in the first folder.")],-1),L=e("p",null,[t("Press "),e("kbd",null,"Esc"),t(" while focused on the Find and Replace panel to clear the pane from your workspace.")],-1),P={href:"https://github.com/pulsar-edit/find-and-replace",target:"_blank",rel:"noopener noreferrer"},J={href:"https://github.com/pulsar-edit/scandal",target:"_blank",rel:"noopener noreferrer"};function M(V,X){const d=s("Tabs"),n=s("ExternalLinkIcon");return f(),m("div",null,[g,b,o(d,{id:"6",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:r(({title:a,value:l,isActive:i})=>[k]),tab1:r(({title:a,value:l,isActive:i})=>[y]),tab2:r(({title:a,value:l,isActive:i})=>[x]),_:1}),w,e("div",F,[v,C,e("p",null,[t("When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, \u2026 $&. Refer to JavaScript's "),e("a",S,[t("guide to regular expressions"),o(n)]),t(" to learn more about regular expression syntax you can use in Pulsar.")])]),N,e("p",null,[t("You can limit a search to a subset of the files in your project by entering a "),e("a",j,[t("glob pattern"),o(n)]),t(' into the "File/Directory pattern" text box. For example, the pattern '),A,t(" would restrict the search to JavaScript files in the "),q,t(' directory. The "globstar" pattern ('),R,t(") can be used to match arbitrarily many subdirectories. For example, "),I,t(" will match "),T,t(", "),W,t(", etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.")]),E,L,e("p",null,[t("The Find and Replace functionality is implemented in the "),e("a",P,[t("find-and-replace"),o(n)]),t(" package and uses the "),e("a",J,[t("scandal"),o(n)]),t(" Node module to do the actual searching.")])])}const Y=p(_,[["render",M],["__file","find-and-replace.html.vue"]]);export{Y as default}; diff --git a/assets/find-and-replace.html.a6a6d2f5.js b/assets/find-and-replace.html.a6a6d2f5.js new file mode 100644 index 0000000000..7391c9d112 --- /dev/null +++ b/assets/find-and-replace.html.a6a6d2f5.js @@ -0,0 +1 @@ +import{_ as n,a as r}from"./find-replace-project.ada882a7.js";import{_ as l,o as s,c as i,a as e,b as t,d as a,f as c,r as d}from"./app.0e1565ce.js";const p={},h=c('Finding and replacing text in your file or project is quick and easy in Atom.
If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press Cmd+FCtrl+F, type in a search string and press Enter (or Cmd+GF3 or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Scott" with the string "Dragon", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
',7),u={class:"custom-container note"},f=e("p",{class:"custom-container-title"},"Note",-1),m=e("p",null,[e("strong",null,"Note:"),t(" Atom uses JavaScript regular expressions to perform regular expression searches.")],-1),_={href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions",target:"_blank",rel:"noopener noreferrer"},b=e("p",null,[t("You can also find and replace throughout your entire project if you invoke the panel with "),e("kbd",{class:"platform-mac"},"Cmd+Shift+F"),e("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+F"),t(".")],-1),g=e("p",null,[e("img",{src:n,alt:"Find and replace text in your project",title:"Find and replace text in your project"})],-1),k=e("p",null,"This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.",-1),y={href:"https://en.wikipedia.org/wiki/Glob_%28programming%29",target:"_blank",rel:"noopener noreferrer"},x=e("code",null,"src/*.js",-1),w=e("code",null,"src",-1),F=e("code",null,"**",-1),v=e("code",null,"docs/**/*.md",-1),S=e("code",null,"docs/a/foo.md",-1),C=e("code",null,"docs/a/b/foo.md",-1),j=e("p",null,[t("When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders "),e("code",null,"/path1/folder1"),t(" and "),e("code",null,"/path2/folder2"),t(" open, you could enter a pattern starting with "),e("code",null,"folder1"),t(" to search only in the first folder.")],-1),q=e("p",null,[t("Press "),e("kbd",{class:"platform-all"},"Esc"),t(" while focused on the Find and Replace panel to clear the pane from your workspace.")],-1),R={href:"https://github.com/atom/find-and-replace",target:"_blank",rel:"noopener noreferrer"},N={href:"https://github.com/atom/scandal",target:"_blank",rel:"noopener noreferrer"};function E(A,T){const o=d("ExternalLinkIcon");return s(),i("div",null,[h,e("div",u,[f,m,e("p",null,[t("When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, \u2026 $&. Refer to JavaScript's "),e("a",_,[t("guide to regular expressions"),a(o)]),t(" to learn more about regular expression syntax you can use in Atom.")])]),b,g,k,e("p",null,[t("You can limit a search to a subset of the files in your project by entering a "),e("a",y,[t("glob pattern"),a(o)]),t(' into the "File/Directory pattern" text box. For example, the pattern '),x,t(" would restrict the search to JavaScript files in the "),w,t(' directory. The "globstar" pattern ('),F,t(") can be used to match arbitrarily many subdirectories. For example, "),v,t(" will match "),S,t(", "),C,t(", etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.")]),j,q,e("p",null,[t("The Find and Replace functionality is implemented in the "),e("a",R,[t("find-and-replace"),a(o)]),t(" package and uses the "),e("a",N,[t("scandal"),a(o)]),t(" Node module to do the actual searching.")])])}const V=l(p,[["render",E],["__file","find-and-replace.html.vue"]]);export{V as default}; diff --git a/assets/find-and-replace.html.f30e892a.js b/assets/find-and-replace.html.f30e892a.js new file mode 100644 index 0000000000..07c918b4db --- /dev/null +++ b/assets/find-and-replace.html.f30e892a.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-282b3c00","path":"/docs/launch-manual/sections/using-pulsar/sections/find-and-replace.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Find and Replace","slug":"find-and-replace","link":"#find-and-replace","children":[]}],"git":{"updatedTime":1678542051000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4}]},"readingTime":{"minutes":2.09,"words":627},"filePathRelative":"docs/launch-manual/sections/using-pulsar/sections/find-and-replace.md"}');export{e as data}; diff --git a/assets/find-replace-file.824478bb.png b/assets/find-replace-file.824478bb.png new file mode 100644 index 0000000000..15db0ae171 Binary files /dev/null and b/assets/find-replace-file.824478bb.png differ diff --git a/assets/find-replace-project.5a3600b1.png b/assets/find-replace-project.5a3600b1.png new file mode 100644 index 0000000000..a8131f497d Binary files /dev/null and b/assets/find-replace-project.5a3600b1.png differ diff --git a/assets/find-replace-project.ada882a7.js b/assets/find-replace-project.ada882a7.js new file mode 100644 index 0000000000..94843e6f94 --- /dev/null +++ b/assets/find-replace-project.ada882a7.js @@ -0,0 +1 @@ +const s="/assets/find-replace-file.824478bb.png",e="/assets/find-replace-project.5a3600b1.png";export{e as _,s as a}; diff --git a/assets/finder.04f876a5.js b/assets/finder.04f876a5.js new file mode 100644 index 0000000000..ac4ab3ce84 --- /dev/null +++ b/assets/finder.04f876a5.js @@ -0,0 +1 @@ +const s="/assets/first-launch.ac3dbc06.png",t="/assets/command-palette.6ea4da90.png",e="/assets/settings.f822be64.png",a="/assets/theme.a183f0e9.png",n="/assets/settings-wrap.da47925c.png",p="/assets/open-file.c788eed5.png",o="/assets/project-view.e5e09e11.png",c="/assets/finder.c50f6161.png";export{s as _,t as a,e as b,a as c,n as d,p as e,o as f,c as g}; diff --git a/assets/finder.c50f6161.png b/assets/finder.c50f6161.png new file mode 100644 index 0000000000..32f61dd1e1 Binary files /dev/null and b/assets/finder.c50f6161.png differ diff --git a/assets/first-launch.ac3dbc06.png b/assets/first-launch.ac3dbc06.png new file mode 100644 index 0000000000..0d261f2fc1 Binary files /dev/null and b/assets/first-launch.ac3dbc06.png differ diff --git a/assets/flight-manual.36cb1750.png b/assets/flight-manual.36cb1750.png new file mode 100644 index 0000000000..5c309398ed Binary files /dev/null and b/assets/flight-manual.36cb1750.png differ diff --git a/assets/folding.6d09f8df.js b/assets/folding.6d09f8df.js new file mode 100644 index 0000000000..5e442b8a35 --- /dev/null +++ b/assets/folding.6d09f8df.js @@ -0,0 +1 @@ +const s="/assets/folding.76f8fd04.png";export{s as _}; diff --git a/assets/folding.76f8fd04.png b/assets/folding.76f8fd04.png new file mode 100644 index 0000000000..6281656b26 Binary files /dev/null and b/assets/folding.76f8fd04.png differ diff --git a/assets/folding.html.3ffe9e96.js b/assets/folding.html.3ffe9e96.js new file mode 100644 index 0000000000..6c249bf7a1 --- /dev/null +++ b/assets/folding.html.3ffe9e96.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-ea7dfef2","path":"/docs/launch-manual/sections/using-pulsar/sections/folding.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Folding","slug":"folding","link":"#folding","children":[]}],"git":{"updatedTime":1669229414000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":1.46,"words":439},"filePathRelative":"docs/launch-manual/sections/using-pulsar/sections/folding.md"}');export{e as data}; diff --git a/assets/folding.html.6d44f0d2.js b/assets/folding.html.6d44f0d2.js new file mode 100644 index 0000000000..1dba035d1f --- /dev/null +++ b/assets/folding.html.6d44f0d2.js @@ -0,0 +1 @@ +import{_ as o}from"./folding.6d09f8df.js";import{_ as t,o as l,c as a,f as e}from"./app.0e1565ce.js";const d={},n=e('If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.
You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the Alt+Cmd+[Alt+Ctrl+[ and Alt+Cmd+]Alt+Ctrl+] keybindings.
To fold everything, use Alt+Cmd+Shift+[Alt+Ctrl+Shift+[ and to unfold everything use Alt+Cmd+Shift+]Alt+Ctrl+Shift+]. You can also fold at a specific indentation level with Cmd+KCtrl+K Cmd+0-9Ctrl+0-9 where the number is the indentation depth.
Finally, you can fold arbitrary sections of your code or text by making a selection and then typing Alt+Cmd+Ctrl+FAlt+Ctrl+F or choosing "Fold Selection" in the Command Palette.
',6),r=[n];function s(i,c){return l(),a("div",null,r)}const p=t(d,[["render",s],["__file","folding.html.vue"]]);export{p as default}; diff --git a/assets/folding.html.aad93fbd.js b/assets/folding.html.aad93fbd.js new file mode 100644 index 0000000000..55d587c248 --- /dev/null +++ b/assets/folding.html.aad93fbd.js @@ -0,0 +1 @@ +import{_ as l}from"./folding.6d09f8df.js";import{_ as r,o as s,c as u,d as c,w as o,a as t,b as e,r as h}from"./app.0e1565ce.js";const f={},b=t("h2",{id:"folding",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#folding","aria-hidden":"true"},"#"),e(" Folding")],-1),m=t("p",null,"If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.",-1),p=t("p",null,[e("You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the "),t("kbd",null,"Alt+Ctrl+["),e(" and "),t("kbd",null,"Alt+Ctrl+]"),e(" keybindings.")],-1),_=t("p",null,[t("img",{src:l,alt:"Code folding example",title:"Code folding example"})],-1),g=t("p",null,[e("To fold everything, use "),t("kbd",null,"Alt+Ctrl+Shift+["),e(" and to unfold everything use "),t("kbd",null,"Alt+Ctrl+Shift+]"),e(". You can also fold at a specific indentation level with "),t("kbd",null,"Ctrl+K"),e(),t("kbd",null,"Ctrl+0-9"),e(" where the number is the indentation depth.")],-1),y=t("p",null,[e("Finally, you can fold arbitrary sections of your code or text by making a selection and then typing "),t("kbd",null,"Alt+Ctrl+F"),e(' or choosing "Fold Selection" in the Command Palette.')],-1),k=t("p",null,[e("You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the "),t("kbd",null,"Alt+Cmd+["),e(" and "),t("kbd",null,"Alt+Cmd+]"),e(" keybindings.")],-1),C=t("p",null,[t("img",{src:l,alt:"Code folding example",title:"Code folding example"})],-1),v=t("p",null,[e("To fold everything, use "),t("kbd",null,"Alt+Cmd+Shift+["),e(" and to unfold everything use "),t("kbd",null,"Alt+Cmd+Shift+]"),e(". You can also fold at a specific indentation level with "),t("kbd",null,"Cmd+K"),e(),t("kbd",null,"Cmd+0-9"),e(" where the number is the indentation depth.")],-1),w=t("p",null,[e("Finally, you can fold arbitrary sections of your code or text by making a selection and then typing "),t("kbd",null,"Alt+Cmd+Ctrl+F"),e(' or choosing "Fold Selection" in the Command Palette.')],-1),A=t("p",null,[e("You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the "),t("kbd",null,"Alt+Ctrl+["),e(" and "),t("kbd",null,"Alt+Ctrl+]"),e(" keybindings.")],-1),x=t("p",null,[t("img",{src:l,alt:"Code folding example",title:"Code folding example"})],-1),F=t("p",null,[e("To fold everything, use "),t("kbd",null,"Alt+Ctrl+Shift+["),e(" and to unfold everything use "),t("kbd",null,"Alt+Ctrl+Shift+]"),e(". You can also fold at a specific indentation level with "),t("kbd",null,"Ctrl+K"),e(),t("kbd",null,"Ctrl+0-9"),e(" where the number is the indentation depth.")],-1),S=t("p",null,[e("Finally, you can fold arbitrary sections of your code or text by making a selection and then typing "),t("kbd",null,"Alt+Ctrl+F"),e(' or choosing "Fold Selection" in the Command Palette.')],-1);function Y(T,B){const a=h("Tabs");return s(),u("div",null,[b,m,c(a,{id:"6",data:[{title:"linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:n,value:i,isActive:d})=>[p,_,g,y]),tab1:o(({title:n,value:i,isActive:d})=>[k,C,v,w]),tab2:o(({title:n,value:i,isActive:d})=>[A,x,F,S]),_:1})])}const P=r(f,[["render",Y],["__file","folding.html.vue"]]);export{P as default}; diff --git a/assets/folding.html.e4e0e277.js b/assets/folding.html.e4e0e277.js new file mode 100644 index 0000000000..4f45a95127 --- /dev/null +++ b/assets/folding.html.e4e0e277.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6252cab2","path":"/docs/atom-archive/using-atom/sections/folding.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Folding","slug":"folding","link":"#folding","children":[]}],"git":{"updatedTime":1664634443000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.87,"words":262},"filePathRelative":"docs/atom-archive/using-atom/sections/folding.md"}');export{e as data}; diff --git a/assets/fonts-in-use.d1baabe5.png b/assets/fonts-in-use.d1baabe5.png new file mode 100644 index 0000000000..7bcb33d5f5 Binary files /dev/null and b/assets/fonts-in-use.d1baabe5.png differ diff --git a/assets/fork.425ac14f.png b/assets/fork.425ac14f.png new file mode 100644 index 0000000000..54d5ce8acb Binary files /dev/null and b/assets/fork.425ac14f.png differ diff --git a/assets/get-help.html.8b9cc126.js b/assets/get-help.html.8b9cc126.js new file mode 100644 index 0000000000..3ec1bfe6f8 --- /dev/null +++ b/assets/get-help.html.8b9cc126.js @@ -0,0 +1 @@ +import{_ as r,o as n,c as s,a as t,b as e,d as o,r as l}from"./app.0e1565ce.js";const i={},u=t("h2",{id:"having-trouble",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#having-trouble","aria-hidden":"true"},"#"),e(" Having trouble?")],-1),h=t("p",null,[e("If you have any issues then please feel free to ask for help from the Pulsar Team or the wider community via any of our "),t("a",{href:"/docs/community"},"Community areas")],-1),c={href:"https://github.com/pulsar-edit/pulsar/issues",target:"_blank",rel:"noopener noreferrer"},p={href:"https://github.com/pulsar-edit/pulsar/issues/new?assignees=&labels=bug%2Ctriage&projects=&template=bug-report.yml",target:"_blank",rel:"noopener noreferrer"};function d(_,f){const a=l("ExternalLinkIcon");return n(),s("div",null,[u,h,t("p",null,[e("If you think you have found a bug then please have a look through our existing "),t("a",c,[e("issues"),o(a)]),e(" and if you can't find anything then please "),t("a",p,[e("create a new bug report"),o(a)]),e(".")])])}const g=r(i,[["render",d],["__file","get-help.html.vue"]]);export{g as default}; diff --git a/assets/get-help.html.a5704781.js b/assets/get-help.html.a5704781.js new file mode 100644 index 0000000000..35659d1606 --- /dev/null +++ b/assets/get-help.html.a5704781.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-495d25f0","path":"/docs/launch-manual/sections/faq/sections/get-help.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Having trouble?","slug":"having-trouble","link":"#having-trouble","children":[]}],"git":{"updatedTime":1694883547000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":0.28,"words":84},"filePathRelative":"docs/launch-manual/sections/faq/sections/get-help.md"}');export{e as data}; diff --git a/assets/gh-discussions.62e6b3a4.png b/assets/gh-discussions.62e6b3a4.png new file mode 100644 index 0000000000..db403e5e17 Binary files /dev/null and b/assets/gh-discussions.62e6b3a4.png differ diff --git a/assets/ghtests.c6b1b477.png b/assets/ghtests.c6b1b477.png new file mode 100644 index 0000000000..b7c8e2237e Binary files /dev/null and b/assets/ghtests.c6b1b477.png differ diff --git a/assets/git-checkout-head.1f356cbf.gif b/assets/git-checkout-head.1f356cbf.gif new file mode 100644 index 0000000000..ad3016cf99 Binary files /dev/null and b/assets/git-checkout-head.1f356cbf.gif differ diff --git a/assets/git-lines.8152cd74.png b/assets/git-lines.8152cd74.png new file mode 100644 index 0000000000..22aa64392d Binary files /dev/null and b/assets/git-lines.8152cd74.png differ diff --git a/assets/git-message.9e959a5d.gif b/assets/git-message.9e959a5d.gif new file mode 100644 index 0000000000..e564366f89 Binary files /dev/null and b/assets/git-message.9e959a5d.gif differ diff --git a/assets/git-status-bar.774ed823.png b/assets/git-status-bar.774ed823.png new file mode 100644 index 0000000000..873305fd22 Binary files /dev/null and b/assets/git-status-bar.774ed823.png differ diff --git a/assets/git-status.748c4f3e.gif b/assets/git-status.748c4f3e.gif new file mode 100644 index 0000000000..1caf44b561 Binary files /dev/null and b/assets/git-status.748c4f3e.gif differ diff --git a/assets/github-amend.a10e79cb.png b/assets/github-amend.a10e79cb.png new file mode 100644 index 0000000000..6272ceae02 Binary files /dev/null and b/assets/github-amend.a10e79cb.png differ diff --git a/assets/github-branch.a90f3b5c.png b/assets/github-branch.a90f3b5c.png new file mode 100644 index 0000000000..79bf7e8b4d Binary files /dev/null and b/assets/github-branch.a90f3b5c.png differ diff --git a/assets/github-checkout.80044dbf.png b/assets/github-checkout.80044dbf.png new file mode 100644 index 0000000000..8d2a7b3e21 Binary files /dev/null and b/assets/github-checkout.80044dbf.png differ diff --git a/assets/github-clone.477d3f4e.png b/assets/github-clone.477d3f4e.png new file mode 100644 index 0000000000..917668d393 Binary files /dev/null and b/assets/github-clone.477d3f4e.png differ diff --git a/assets/github-commit-detail.705f4c6d.png b/assets/github-commit-detail.705f4c6d.png new file mode 100644 index 0000000000..82d63f4d82 Binary files /dev/null and b/assets/github-commit-detail.705f4c6d.png differ diff --git a/assets/github-commit-preview.be41e452.png b/assets/github-commit-preview.be41e452.png new file mode 100644 index 0000000000..c5346b3c25 Binary files /dev/null and b/assets/github-commit-preview.be41e452.png differ diff --git a/assets/github-commit-with-co-authors.45bf6f00.png b/assets/github-commit-with-co-authors.45bf6f00.png new file mode 100644 index 0000000000..159b0181bd Binary files /dev/null and b/assets/github-commit-with-co-authors.45bf6f00.png differ diff --git a/assets/github-commit.f31064ae.png b/assets/github-commit.f31064ae.png new file mode 100644 index 0000000000..c8a2e56065 Binary files /dev/null and b/assets/github-commit.f31064ae.png differ diff --git a/assets/github-create-a-pull-request.7504093b.png b/assets/github-create-a-pull-request.7504093b.png new file mode 100644 index 0000000000..7345a2c425 Binary files /dev/null and b/assets/github-create-a-pull-request.7504093b.png differ diff --git a/assets/github-desktop.a5a19b8f.png b/assets/github-desktop.a5a19b8f.png new file mode 100644 index 0000000000..2635ca5cab Binary files /dev/null and b/assets/github-desktop.a5a19b8f.png differ diff --git a/assets/github-discard.120eb488.png b/assets/github-discard.120eb488.png new file mode 100644 index 0000000000..ca8cbdabb8 Binary files /dev/null and b/assets/github-discard.120eb488.png differ diff --git a/assets/github-fetch-pull.d98174e7.png b/assets/github-fetch-pull.d98174e7.png new file mode 100644 index 0000000000..a6e4746e57 Binary files /dev/null and b/assets/github-fetch-pull.d98174e7.png differ diff --git a/assets/github-initialize.dc7a1ee0.png b/assets/github-initialize.dc7a1ee0.png new file mode 100644 index 0000000000..d993ef590f Binary files /dev/null and b/assets/github-initialize.dc7a1ee0.png differ diff --git a/assets/github-open-git-panel.15e560ef.png b/assets/github-open-git-panel.15e560ef.png new file mode 100644 index 0000000000..d6e45415e9 Binary files /dev/null and b/assets/github-open-git-panel.15e560ef.png differ diff --git a/assets/github-open-issue-or-pull-request.7503ac93.png b/assets/github-open-issue-or-pull-request.7503ac93.png new file mode 100644 index 0000000000..a0a66deebc Binary files /dev/null and b/assets/github-open-issue-or-pull-request.7503ac93.png differ diff --git a/assets/github-open-review-from-diff.193039f2.png b/assets/github-open-review-from-diff.193039f2.png new file mode 100644 index 0000000000..8cbb783f41 Binary files /dev/null and b/assets/github-open-review-from-diff.193039f2.png differ diff --git a/assets/github-package.html.3c4fde2e.js b/assets/github-package.html.3c4fde2e.js new file mode 100644 index 0000000000..9f01889857 --- /dev/null +++ b/assets/github-package.html.3c4fde2e.js @@ -0,0 +1,2 @@ +import{_ as i,a as s,b as r,c as n,d as l,e as c,f as h,g as u,h as m,i as p,j as d,k as g,l as b,m as f,n as k,o as w,p as q,q as v,r as y,s as _,t as C,u as R,v as P,w as I,x,y as G}from"./github-review-reply.6d405e4b.js";import{_ as T,o as H,c as O,a as t,b as e,d as A,f as o,r as S}from"./app.0e1565ce.js";const N={},V=o('The GitHub package brings Git and GitHub integration right inside Pulsar.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Pulsar and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the LNX/WIN: Cmd-Left - MAC: Ctrl-Left or LNX/WIN: Ctrl-Right - MAC: Cmd-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "\u{1F464}\u2795" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
The github package brings Git and GitHub integration right inside Atom.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Atom and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the Cmd-LeftCtrl-Left or Cmd-RightCtrl-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "\u{1F464}\u2795" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
Below is a list of some useful terms we use with regard to Atom.
A buffer is the text content of a file in Atom. It's basically the same as a file for most descriptions, but it's the version Atom has in memory. For instance, you can change the text of a buffer and it isn't written to its associated file until you save it.
A command is a bit of functionality in Atom that can be triggered by the user either through a keybinding or a menu item.
Docks are collapsible pane containers that attach to the left, right, and bottom sides of the Atom window.
Examples:
A key combination is some combination or sequence of keys that are pressed to perform a task.
Examples:
A key sequence is a special case of a key combination. It is a key combination that consists of keys that must be pressed and released in sequence. Ctrl+K Down is a key sequence. Alt+S is not a key sequence because it is two keys that are pressed and released together rather than in succession.
A keybinding is the mapping of a key combination, such as Ctrl+Enter to an Atom command.
A keymap is a collection of keybindings. It can also refer to a file or files containing keybindings for an Atom package or Atom itself.
A pane is a visual section of the editor space. Each pane can hold multiple pane items. There is always at least one pane in each Atom window.
A section of the Atom UI that can contain multiple panes.
Some item, often an editor, that is displayed within a pane. In the default configuration of Atom, pane items are represented by tabs at the top of each pane.
',6),f={class:"custom-container note"},u=e("p",{class:"custom-container-title"},"Note",-1),b=e("strong",null,"Note:",-1),k={href:"https://github.com/atom/tabs",target:"_blank",rel:"noopener noreferrer"},y=e("h4",{id:"panel",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#panel","aria-hidden":"true"},"#"),a(" Panel")],-1),g=e("p",null,"A piece of the Atom UI that is outside the editor space.",-1),_=e("p",null,"Examples:",-1),A=e("ul",null,[e("li",null,"Find and Replace"),e("li",null,"Keybinding Resolver")],-1);function x(w,C){const o=n("RouterLink"),s=n("ExternalLinkIcon");return l(),h("div",null,[p,e("p",null,[a("An Atom plugin. There is a bunch more information in the section on "),t(o,{to:"/using-atom/sections/atom-packages/"},{default:c(()=>[a("Atom Packages")]),_:1}),a(".")]),m,e("div",f,[u,e("p",null,[b,a(` The reason why we don't call them "tabs" is because you can disable the `),e("a",k,[a("tabs package"),t(s)]),a(" and then there aren't any tabs. For a similar reason, we don't call them files because some things can be shown in a pane that aren't files, like the Settings View.")])]),y,g,_,A])}const I=r(d,[["render",x],["__file","glossary.html.vue"]]);export{I as default}; diff --git a/assets/go-to-declaration-example.62447add.mp4 b/assets/go-to-declaration-example.62447add.mp4 new file mode 100644 index 0000000000..89890c1b93 Binary files /dev/null and b/assets/go-to-declaration-example.62447add.mp4 differ diff --git a/assets/go-to-declaration-example.87adc60b.webm b/assets/go-to-declaration-example.87adc60b.webm new file mode 100644 index 0000000000..13f0f8f3a8 Binary files /dev/null and b/assets/go-to-declaration-example.87adc60b.webm differ diff --git a/assets/goto.8ea021d6.png b/assets/goto.8ea021d6.png new file mode 100644 index 0000000000..cbe6bc7a09 Binary files /dev/null and b/assets/goto.8ea021d6.png differ diff --git a/assets/grammar.a72cafde.png b/assets/grammar.a72cafde.png new file mode 100644 index 0000000000..cd991e676c Binary files /dev/null and b/assets/grammar.a72cafde.png differ diff --git a/assets/grammar.c16b8cd6.js b/assets/grammar.c16b8cd6.js new file mode 100644 index 0000000000..f55ff8a623 --- /dev/null +++ b/assets/grammar.c16b8cd6.js @@ -0,0 +1 @@ +const a="/assets/grammar.a72cafde.png";export{a as _}; diff --git a/assets/grammar.html.63ce206c.js b/assets/grammar.html.63ce206c.js new file mode 100644 index 0000000000..f274055158 --- /dev/null +++ b/assets/grammar.html.63ce206c.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-3ae8d9e8","path":"/docs/atom-archive/using-atom/sections/grammar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Grammar","slug":"grammar","link":"#grammar","children":[]}],"git":{"updatedTime":1664634443000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.7,"words":211},"filePathRelative":"docs/atom-archive/using-atom/sections/grammar.md"}');export{a as data}; diff --git a/assets/grammar.html.b1bb0d35.js b/assets/grammar.html.b1bb0d35.js new file mode 100644 index 0000000000..3f15fc6b93 --- /dev/null +++ b/assets/grammar.html.b1bb0d35.js @@ -0,0 +1 @@ +import{_ as n}from"./grammar.c16b8cd6.js";import{_ as s,o as l,c as m,a as e,b as t,d as a,w as c,r as o}from"./app.0e1565ce.js";const h={},d=e("h3",{id:"grammar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#grammar","aria-hidden":"true"},"#"),t(" Grammar")],-1),f=e("p",null,[t("When you load a file, Atom does a little work to try to figure out what type of file it is. Largely this is accomplished by looking at its file extension ("),e("code",null,".md"),t(" is generally a Markdown file, etc), though sometimes it has to inspect the content a bit to figure it out.")],-1),u=e("p",null,[t(`When you open a file and Atom can't determine a grammar for the file, it will default to "Plain Text", which is the simplest one. If it does default to "Plain Text", picks the wrong grammar for the file, or if for any reason you wish to change the selected grammar, you can pull up the Grammar Selector with `),e("kbd",{class:"platform-all"},"Ctrl+Shift+L"),t(".")],-1),p=e("p",null,[e("img",{src:n,alt:"Grammar Selector",title:"Grammar Selector"})],-1),_=e("p",null,"When the grammar of a file is changed, Atom will remember that for the current session.",-1),g={href:"https://github.com/atom/atom/tree/master/packages/grammar-selector",target:"_blank",rel:"noopener noreferrer"};function w(k,y){const r=o("RouterLink"),i=o("ExternalLinkIcon");return l(),m("div",null,[d,e("p",null,[t('The "grammar" of a file is what language Atom has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in '),a(r,{to:"/using-atom/sections/snippets/"},{default:c(()=>[t("Snippets")]),_:1}),t(".")]),f,u,p,_,e("p",null,[t("The Grammar Selector functionality is implemented in the "),e("a",g,[t("grammar-selector"),a(i)]),t(" package.")])])}const G=s(h,[["render",w],["__file","grammar.html.vue"]]);export{G as default}; diff --git a/assets/grammar.html.bc810e1f.js b/assets/grammar.html.bc810e1f.js new file mode 100644 index 0000000000..783e42adbe --- /dev/null +++ b/assets/grammar.html.bc810e1f.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-0af9c8e1","path":"/docs/launch-manual/sections/using-pulsar/sections/grammar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Grammar","slug":"grammar","link":"#grammar","children":[]}],"git":{"updatedTime":1669229414000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":0.69,"words":206},"filePathRelative":"docs/launch-manual/sections/using-pulsar/sections/grammar.md"}');export{a as data}; diff --git a/assets/grammar.html.e25e2511.js b/assets/grammar.html.e25e2511.js new file mode 100644 index 0000000000..e7fcd7ae9a --- /dev/null +++ b/assets/grammar.html.e25e2511.js @@ -0,0 +1 @@ +import{_ as r}from"./grammar.c16b8cd6.js";import{_ as o,o as i,c as s,a as t,b as e,d as n,f as l,r as m}from"./app.0e1565ce.js";const c={},h=l('The "grammar" of a file is what language Pulsar has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in Snippets.
When you load a file, Pulsar does a little work to try to figure out what type of file it is. Largely this is accomplished by looking at its file extension (.md
is generally a Markdown file, etc.), though sometimes it has to inspect the content a bit to figure it out.
When you open a file and Pulsar can't determine a grammar for the file, it will default to "Plain Text", which is the simplest one. If it does default to "Plain Text", picks the wrong grammar for the file, or if for any reason you wish to change the selected grammar, you can pull up the Grammar Selector with Ctrl+Shift+L.
When the grammar of a file is changed, Atom will remember that for the current session.
',6),u={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/grammar-selector",target:"_blank",rel:"noopener noreferrer"};function d(p,f){const a=m("ExternalLinkIcon");return i(),s("div",null,[h,t("p",null,[e("The Grammar Selector functionality is implemented in the "),t("a",u,[e("grammar-selector"),n(a)]),e(" package.")])])}const k=o(c,[["render",d],["__file","grammar.html.vue"]]);export{k as default}; diff --git a/assets/hacking-on-atom-core.html.65b1ff88.js b/assets/hacking-on-atom-core.html.65b1ff88.js new file mode 100644 index 0000000000..bd07aa130a --- /dev/null +++ b/assets/hacking-on-atom-core.html.65b1ff88.js @@ -0,0 +1,31 @@ +import{_ as u,o as h,c as m,a as e,b as n,d as o,w as l,f as d,r as c}from"./app.0e1565ce.js";const p={},b=e("h3",{id:"hacking-on-atom-core",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#hacking-on-atom-core","aria-hidden":"true"},"#"),n(" Hacking on Atom Core")],-1),g=e("p",null,"If you're hitting a bug in Atom or just want to experiment with adding a feature to the core of the system, you'll want to run Atom in Dev Mode with access to a local copy of the Atom source.",-1),_=e("h4",{id:"fork-the-atom-atom-repository",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#fork-the-atom-atom-repository","aria-hidden":"true"},"#"),n(" Fork the atom/atom repository")],-1),v={href:"https://help.github.com/articles/fork-a-repo/",target:"_blank",rel:"noopener noreferrer"},f=d(`Once you've set up your fork of the atom/atom repository, you can clone it to your local machine:
$ git clone git@github.com:<em>your-username</em>/atom.git
+
From there, you can navigate into the directory where you've cloned the Atom source code and run the bootstrap script to install all the required dependencies:
`,4),y=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ cd where-you-cloned-atom +$ script/bootstrap +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),w=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ cd where-you-cloned-atom +$ script/bootstrap +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),x=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ cd where-you-cloned-atom +$ script\\bootstrap +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),k=d(`Once you have a local copy of Atom cloned and bootstrapped, you can then run Atom in Development Mode. But first, if you cloned Atom to somewhere other than ~/github/atom
%USERPROFILE%\\github\\atom
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Atom. To run Atom in Dev Mode, use the --dev
parameter from the terminal:
$ atom --dev <em>path-to-open</em>
+
Note
Note: If the atom command does not respond in the terminal, then try atom-dev or atom-beta. The suffix depends upon the particular source code that was cloned.
There are a couple benefits of running Atom in Dev Mode:
`,5),A=d('ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Atom is run using the source code from your local atom/atom
repository. This means that you don't have to run script/build
script\\build
every time you change code. Just restart Atom \u{1F44D}~/.atom/dev/packages
%USERPROFILE%\\.atom\\dev\\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.In order to run Atom Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <em>path-to-your-local-atom-repo</em>
+$ atom --test spec
+
In order to build Atom from source, you need to have a number of other requirements and take additional steps.
`,5),E=e("p",null,"Ubuntu LTS 16.04 64-bit is the recommended platform.",-1),I=e("h5",{id:"requirements",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#requirements","aria-hidden":"true"},"#"),n(" Requirements")],-1),C=e("li",null,"OS with 64-bit or 32-bit architecture",-1),$=e("li",null,"C++11 toolchain",-1),D=e("li",null,"Git",-1),O={href:"https://github.com/creationix/nvm",target:"_blank",rel:"noopener noreferrer"},R=e("li",null,[n("npm 6.12 or later (run "),e("code",null,"npm install -g npm"),n(")")],-1),M=e("li",null,"Python 2.6.x, 2.7.x or 3.5+",-1),V={href:"https://wiki.gnome.org/Projects/Libsecret",target:"_blank",rel:"noopener noreferrer"},j=e("p",null,"For more details, scroll down to find how to setup a specific Linux distro.",-1),N=e("h6",{id:"ubuntu-debian",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#ubuntu-debian","aria-hidden":"true"},"#"),n(" Ubuntu / Debian")],-1),L=e("ul",null,[e("li",null,[e("p",null,"Install GNOME headers and other basic prerequisites:"),e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo apt-get install build-essential git libsecret-1-dev fakeroot rpm libx11-dev libxkbfile-dev +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])])]),e("li",null,[e("p",null,[n("If "),e("code",null,"script/build"),n(" exits with an error, you may need to install a newer C++ compiler with C++11:")]),e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo add-apt-repository ppa:ubuntu-toolchain-r/test +$ sudo apt-get update +$ sudo apt-get install gcc-5 g++-5 +$ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 80 --slave /usr/bin/g++ g++ /usr/bin/g++-5 +$ sudo update-alternatives --config gcc # choose gcc-5 from the list +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])])])],-1),W=e("h6",{id:"fedora-22",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#fedora-22","aria-hidden":"true"},"#"),n(" Fedora 22+")],-1),U=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo dnf --assumeyes install make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools libX11-devel libxkbfile-devel +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),F=e("h6",{id:"fedora-21-centos-rhel",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#fedora-21-centos-rhel","aria-hidden":"true"},"#"),n(" Fedora 21 / CentOS / RHEL")],-1),B=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo yum install -y make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),G=e("h6",{id:"arch",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#arch","aria-hidden":"true"},"#"),n(" Arch")],-1),q=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`sudo pacman -S --needed gconf base-devel git nodejs npm libsecret python libx11 libxkbfile +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),z=e("h6",{id:"slackware",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#slackware","aria-hidden":"true"},"#"),n(" Slackware")],-1),H=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sbopkg -k -i node -i atom +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),K=e("h6",{id:"opensuse",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#opensuse","aria-hidden":"true"},"#"),n(" openSUSE")],-1),Y=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo zypper install nodejs nodejs-devel make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools libX11-devel libxkbfile-devel +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),X=e("h5",{id:"requirements-1",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#requirements-1","aria-hidden":"true"},"#"),n(" Requirements")],-1),J=e("li",null,"macOS 10.9 or later",-1),Q={href:"https://github.com/creationix/nvm",target:"_blank",rel:"noopener noreferrer"},Z=e("li",null,[n("npm 6.12 or later (run "),e("code",null,"npm install -g npm"),n(")")],-1),ee=e("li",null,"Python v2.6.x, v2.7.x or v3.5+",-1),ne={href:"https://developer.apple.com/xcode/downloads/",target:"_blank",rel:"noopener noreferrer"},oe=e("code",null,"xcode-select --install",-1),te=e("li",null,[e("p",null,"Node.js 10.12 or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom)")],-1),le=e("li",null,[e("p",null,[n("npm 6.12 or later (run "),e("code",null,"npm install -g npm"),n(")")])],-1),ie=e("p",null,"Python v2.6.x, v2.7.x, or v3.5+",-1),ae={href:"https://www.microsoft.com/en-us/search/shop/apps?q=python+software+foundation&devicetype=pc&category=Developer+tools%5cDevelopment+kits&Price=0&MaturityRating=ESRB%3aE",target:"_blank",rel:"noopener noreferrer"},se=e("li",null,[n("Download Python from https://www.python.org/downloads/. "),e("ul",null,[e("li",null,'For Python 2, be sure to install in the default location, or check "Add Python 2.x to PATH" before installing.'),e("li",null,[n('For Python 3, check "Add Python 3.x to PATH", or change the install path to '),e("code",null,"[Your_Drive_Letter]:\\Python37"),n(" e.g. "),e("code",null,"C:\\Python37"),n(", (even if your version of Python 3 isn't 3.7, that's one place where the scripts will look.)")]),e("li",null,[n("If python isn't found by the bootstrap script, create a symbolic link to the directory containing "),e("code",null,"python.exe"),n(" using e.g.: "),e("code",null,"mklink /d %SystemDrive%\\Python27 D:\\elsewhere\\Python27"),n("(Links should be set at either "),e("code",null,"%SystemDrive%\\Python27"),n(" or "),e("code",null,"%SystemDrive%\\Python37"),n(", regardless of what version of Python you actually have.)")])])],-1),re=e("p",null,"C++ build tools:",-1),de=e("strong",null,"Option 1:",-1),ce={href:"https://www.npmjs.com/package/windows-build-tools",target:"_blank",rel:"noopener noreferrer"},ue=e("code",null,"npm install --global windows-build-tools@4",-1),he=e("strong",null,"Option 2:",-1),me={href:"https://visualstudio.microsoft.com/visual-cpp-build-tools/",target:"_blank",rel:"noopener noreferrer"},pe=e("strong",null,"Option 3:",-1),be={href:"https://www.visualstudio.com/downloads/",target:"_blank",rel:"noopener noreferrer"},ge=e("p",null,"Also ensure that:",-1),_e=e("ul",null,[e("li",null,"The default installation folder is chosen so the build tools can find it"),e("li",null,"If using Visual Studio make sure Visual C++ support is selected/installed"),e("li",null,"If using Visual C++ Build Tools make sure a Windows SDK (Windows 8 SDK or Windows 10 SDK) is selected/installed"),e("li",null,[n("A "),e("code",null,"git"),n(" command is in your path")]),e("li",null,[n("Set the "),e("code",null,"GYP_MSVS_VERSION"),n(" environment variable to the Visual Studio/Build Tools version ("),e("code",null,"2015"),n(" or "),e("code",null,"2017"),n(".) e.g. "),e("code",null,'[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")'),n(" in PowerShell (or set it in Windows advanced system settings).")])],-1),ve=e("h5",{id:"instructions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#instructions","aria-hidden":"true"},"#"),n(" Instructions")],-1),fe=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ script/build +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),ye=e("p",null,[n("To also install the newly built application, use the "),e("code",null,"--create-debian-package"),n(" or "),e("code",null,"--create-rpm-package"),n(" option and then install the generated package via the system package manager.")],-1),we=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ script/build +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),xe=e("p",null,[n("To also install the newly built application, use "),e("code",null,"script/build --install"),n(".")],-1),ke=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ script\\build +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Ae=e("p",null,[n("To also install the newly built application, use "),e("code",null,"script\\build --create-windows-installer"),n(" and launch one of the generated installers.")],-1),Se=e("h5",{id:"script-build-options",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#script-build-options","aria-hidden":"true"},"#"),n(),e("code",null,"script/build"),n(" Options")],-1),Te=e("ul",null,[e("li",null,[e("code",null,"--compress-artifacts"),n(": zips the generated application as "),e("code",null,"out/atom-{arch}.tar.gz"),n(".")]),e("li",null,[e("code",null,"--create-debian-package"),n(": creates a .deb package as "),e("code",null,"out/atom-{arch}.deb")]),e("li",null,[e("code",null,"--create-rpm-package"),n(": creates a .rpm package as "),e("code",null,"out/atom-{arch}.rpm")]),e("li",null,[e("code",null,"--install[=dir]"),n(": installs the application in "),e("code",null,"${dir}"),n("; "),e("code",null,"${dir}"),n(" defaults to "),e("code",null,"/usr/local"),n(".")])],-1),Pe=e("ul",null,[e("li",null,[e("code",null,"--code-sign"),n(": signs the application with the GitHub certificate specified in "),e("code",null,"$ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL"),n(".")]),e("li",null,[e("code",null,"--compress-artifacts"),n(": zips the generated application as "),e("code",null,"out/atom-mac.zip"),n(".")]),e("li",null,[e("code",null,"--install[=dir]"),n(": installs the application at "),e("code",null,"${dir}/Atom.app"),n(" for dev and stable versions or at "),e("code",null,"${dir}/Atom-Beta.app"),n(" for beta versions; "),e("code",null,"${dir}"),n(" defaults to "),e("code",null,"/Applications"),n(".")])],-1),Ee=e("ul",null,[e("li",null,[e("code",null,"--code-sign"),n(": signs the application with the GitHub certificate specified in "),e("code",null,"$WIN_P12KEY_URL"),n(".")]),e("li",null,[e("code",null,"--compress-artifacts"),n(": zips the generated application as "),e("code",null,"out\\atom-windows.zip"),n(".")]),e("li",null,[e("code",null,"--create-windows-installer"),n(": creates an "),e("code",null,".exe"),n(" and two "),e("code",null,".nupkg"),n(" packages in the "),e("code",null,"out"),n(" directory.")]),e("li",null,[e("code",null,"--install[=dir]"),n(": installs the application in "),e("code",null,"${dir}\\Atom\\app-dev"),n("; "),e("code",null,"${dir}"),n(" defaults to "),e("code",null,"%LOCALAPPDATA%"),n(".")])],-1),Ie=e("h5",{id:"troubleshooting",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#troubleshooting","aria-hidden":"true"},"#"),n(" Troubleshooting")],-1),Ce=e("h6",{id:"typeerror-unable-to-watch-path",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typeerror-unable-to-watch-path","aria-hidden":"true"},"#"),n(" TypeError: Unable to watch path")],-1),$e=e("p",null,"If you get following error with a big traceback right after Atom starts:",-1),De=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`TypeError: Unable to watch path +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Oe=e("p",null,"you have to increase number of watched files by inotify. For testing if this is the reason for this error you can execute:",-1),Re=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo sysctl fs.inotify.max_user_watches=32768 +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Me=e("p",null,"then restart Atom. If Atom now works fine, you can make this setting permanent:",-1),Ve=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ echo 32768 | sudo tee -a /proc/sys/fs/inotify/max_user_watches +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),je={href:"https://github.com/atom/atom/issues/2082",target:"_blank",rel:"noopener noreferrer"},Ne=e("h6",{id:"usr-bin-env-node-no-such-file-or-directory",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#usr-bin-env-node-no-such-file-or-directory","aria-hidden":"true"},"#"),n(" /usr/bin/env: node: No such file or directory")],-1),Le=e("p",null,[n("If you get this notice when attempting to run any script, you either do not have Node.js installed, or node isn't identified as Node.js on your machine. If it's the latter, this might be caused by installing Node.js via the distro package manager and not nvm, so entering "),e("code",null,"sudo ln -s /usr/bin/nodejs /usr/bin/node"),n(" into your terminal may fix the issue. On some variants (mostly Debian based distros) you can use "),e("code",null,"update-alternatives"),n(" too:")],-1),We=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 1 --slave /usr/bin/js js /usr/bin/nodejs +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Ue=e("h6",{id:"attributeerror-module-object-has-no-attribute-script-main",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#attributeerror-module-object-has-no-attribute-script-main","aria-hidden":"true"},"#"),n(" AttributeError: 'module' object has no attribute 'script_main'")],-1),Fe=e("p",null,"If you get following error with a big traceback while building Atom:",-1),Be=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`sys.exit(gyp.script_main()) AttributeError: 'module' object has no attribute 'script_main' gyp ERR! +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Ge=e("p",null,"you need to uninstall the system version of gyp.",-1),qe=e("p",null,"On Fedora you would do the following:",-1),ze=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo yum remove gyp +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),He=e("h6",{id:"linux-build-error-reports",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#linux-build-error-reports","aria-hidden":"true"},"#"),n(" Linux build error reports")],-1),Ke={href:"https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues",target:"_blank",rel:"noopener noreferrer"},Ye={href:"https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Amac&type=Issues",target:"_blank",rel:"noopener noreferrer"},Xe=e("h6",{id:"common-errors",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#common-errors","aria-hidden":"true"},"#"),n(" Common Errors")],-1),Je=e("li",null,[e("p",null,[e("code",null,"node is not recognized")]),e("ul",null,[e("li",null,[n("If you just installed Node.js, you'll need to restart Command Prompt before the "),e("code",null,"node"),n(" command is available on your path.")])])],-1),Qe=e("li",null,[e("p",null,[e("code",null,"msbuild.exe failed with exit code: 1")]),e("ul",null,[e("li",null,[n("If using "),e("strong",null,"Visual Studio"),n(", ensure you have the "),e("strong",null,"Visual C++"),n(" component installed. Go into Add/Remove Programs, select Visual Studio, press Modify, and then check the Visual C++ box.")]),e("li",null,[n("If using "),e("strong",null,"Visual C++ Build Tools"),n(", ensure you have the "),e("strong",null,"Windows 8 SDK"),n(" or "),e("strong",null,"Windows 10 SDK"),n(' component installed. Go into Add/Remove Programs, select Visual C++ Build Tools, press Modify and then check the "Windows 8 SDK" or "Windows 10 SDK" box.')])])],-1),Ze=e("p",null,[e("code",null,"script\\build"),n(" stops with no error or warning shortly after displaying the versions of node, npm and Python")],-1),en=e("li",null,[n("Make sure that the path where you have checked out Atom does not include a space. For example, use "),e("code",null,"C:\\atom"),n(" instead of "),e("code",null,"C:\\my stuff\\atom"),n(".")],-1),nn=e("code",null,"C:\\atom",-1),on={href:"https://github.com/atom/atom/issues/2200",target:"_blank",rel:"noopener noreferrer"},tn=e("p",null,[e("code",null,"error MSB4025: The project file could not be loaded. Invalid character in the given encoding.")],-1),ln=e("code",null,"%USERPROFILE%",-1),an={href:"https://bugs.chromium.org/p/gyp/issues/list",target:"_blank",rel:"noopener noreferrer"},sn=e("ul",null,[e("li",null,"https://github.com/TooTallNate/node-gyp/issues/297"),e("li",null,"https://bugs.chromium.org/p/gyp/issues/detail?id=393")],-1),rn=e("li",null,[e("p",null,[e("code",null,"'node_modules\\.bin\\npm' is not recognized as an internal or external command, operable program or batch file.")]),e("ul",null,[e("li",null,[n("This occurs if the previous build left things in a bad state. Run "),e("code",null,"script\\clean"),n(" and then "),e("code",null,"script\\build"),n(" again.")])])],-1),dn=e("li",null,[e("p",null,[e("code",null,"script\\build"),n(" stops at installing runas with "),e("code",null,"Failed at the runas@x.y.z install script.")]),e("ul",null,[e("li",null,"See the next item.")])],-1),cn=e("li",null,[e("p",null,[e("code",null,"error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.")]),e("ul",null,[e("li",null,[n("Try setting the "),e("code",null,"GYP_MSVS_VERSION"),n(" environment variable to "),e("strong",null,"2015"),n(" or "),e("strong",null,"2017"),n(" depending on what version of Visual Studio/Build Tools is installed and then "),e("code",null,"script\\clean"),n(" followed by "),e("code",null,"script\\build"),n(" (re-open the Command Prompt if you set the variable using the GUI).")])])],-1),un=e("li",null,[e("p",null,[e("code",null,"'node-gyp' is not recognized as an internal or external command, operable program or batch file.")]),e("ul",null,[e("li",null,[n("Try running "),e("code",null,"npm install -g node-gyp"),n(", and run "),e("code",null,"script\\build"),n(" again.")])])],-1),hn=e("li",null,[e("p",null,[n("Other "),e("code",null,"node-gyp"),n(" errors on first build attempt, even though the right Node.js and Python versions are installed.")]),e("ul",null,[e("li",null,"Do try the build command one more time as experience shows it often works on second try in many cases.")])],-1),mn=e("h6",{id:"windows-build-error-reports",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#windows-build-error-reports","aria-hidden":"true"},"#"),n(" Windows build error reports")],-1),pn={href:"https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Awindows&type=Issues",target:"_blank",rel:"noopener noreferrer"},bn=e("li",null,"If it hasn't, please open a new issue with your Windows version, architecture (x86 or x64), and a text dump of your build output, including the Node.js and Python versions.",-1);function gn(_n,vn){const t=c("ExternalLinkIcon"),r=c("Tabs");return h(),m("div",null,[b,g,_,e("p",null,[n("Follow the "),e("a",v,[n("GitHub Help instructions on how to fork a repo"),o(t)]),n(".")]),f,o(r,{id:"22",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:l(({title:i,value:a,isActive:s})=>[y]),tab1:l(({title:i,value:a,isActive:s})=>[w]),tab2:l(({title:i,value:a,isActive:s})=>[x]),_:1}),k,e("ol",null,[A,e("li",null,[n("Packages that contain stylesheets, such as syntax themes, will have those stylesheets automatically reloaded by the "),e("a",S,[n("dev-live-reload"),o(t)]),n(" package. This does not live reload JavaScript or CoffeeScript files \u2014 you'll need to reload the window ("),T,n(") to see changes to those.")])]),P,o(r,{id:"78",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:l(({title:i,value:a,isActive:s})=>[E,I,e("ul",null,[C,$,D,e("li",null,[n("Node.js 10.12 or later (we recommend installing it via "),e("a",O,[n("nvm"),o(t)]),n(")")]),R,M,e("li",null,[n("Development headers for "),e("a",V,[n("libsecret"),o(t)]),n(".")])]),j,N,L,W,U,F,B,G,q,z,H,K,Y]),tab1:l(({title:i,value:a,isActive:s})=>[X,e("ul",null,[J,e("li",null,[n("Node.js 10.12 or later (we recommend installing it via "),e("a",Q,[n("nvm"),o(t)]),n(")")]),Z,ee,e("li",null,[n("Command Line Tools for "),e("a",ne,[n("Xcode"),o(t)]),n(" (run "),oe,n(" to install)")])])]),tab2:l(({title:i,value:a,isActive:s})=>[e("ul",null,[te,le,e("li",null,[ie,e("ul",null,[e("li",null,[e("a",ae,[n("Get Python from the Microsoft Store"),o(t)]),n(", or")]),se])]),e("li",null,[re,e("ul",null,[e("li",null,[de,n(),e("a",ce,[n("windows-build-tools"),o(t)]),n(' - From an elevated Powershell window (right click and "run as Administrator") do: '),ue,n(" to install")]),e("li",null,[he,n(),e("a",me,[n("Visual C++ Build Tools 2015 or 2017"),o(t)])]),e("li",null,[pe,n(),e("a",be,[n("Visual Studio 2015 or 2017"),o(t)]),n(" (Community Edition or better)")])]),ge,_e])])]),_:1}),ve,o(r,{id:"300",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:l(({title:i,value:a,isActive:s})=>[fe,ye]),tab1:l(({title:i,value:a,isActive:s})=>[we,xe]),tab2:l(({title:i,value:a,isActive:s})=>[ke,Ae]),_:1}),Se,o(r,{id:"323",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:l(({title:i,value:a,isActive:s})=>[Te]),tab1:l(({title:i,value:a,isActive:s})=>[Pe]),tab2:l(({title:i,value:a,isActive:s})=>[Ee]),_:1}),Ie,o(r,{id:"395",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:l(({title:i,value:a,isActive:s})=>[Ce,$e,De,Oe,Re,Me,Ve,e("p",null,[n("See also "),e("a",je,[n("#2082"),o(t)]),n(".")]),Ne,Le,We,Ue,Fe,Be,Ge,qe,ze,He,e("p",null,[n("Use "),e("a",Ke,[n("this search"),o(t)]),n(" to get a list of reports about build errors on Linux.")])]),tab1:l(({title:i,value:a,isActive:s})=>[e("p",null,[n("Use "),e("a",Ye,[n("this search"),o(t)]),n(" to get a list of reports about build errors on macOS.")])]),tab2:l(({title:i,value:a,isActive:s})=>[Xe,e("ul",null,[Je,Qe,e("li",null,[Ze,e("ul",null,[en,e("li",null,[n("Try moving the repository to "),nn,n(". Most likely, the path is too long. See "),e("a",on,[n("issue #2200"),o(t)]),n(".")])])]),e("li",null,[tn,e("ul",null,[e("li",null,[n("This can occur because your home directory ("),ln,n(") has non-ASCII characters in it. This is a bug in "),e("a",an,[n("gyp"),o(t)]),n(" which is used to build native Node.js modules and there is no known workaround. "),sn])])]),rn,dn,cn,un,hn]),mn,e("ul",null,[e("li",null,[n("If all else fails, use "),e("a",pn,[n("this search"),o(t)]),n(" to get a list of reports about build errors on Windows, and see if yours has already been reported.")]),bn])]),_:1})])}const yn=u(p,[["render",gn],["__file","hacking-on-atom-core.html.vue"]]);export{yn as default}; diff --git a/assets/hacking-on-atom-core.html.db966159.js b/assets/hacking-on-atom-core.html.db966159.js new file mode 100644 index 0000000000..1c4ae5edad --- /dev/null +++ b/assets/hacking-on-atom-core.html.db966159.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-bb85f9fa","path":"/docs/atom-archive/hacking-atom/sections/hacking-on-atom-core.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Hacking on Atom Core","slug":"hacking-on-atom-core","link":"#hacking-on-atom-core","children":[]}],"git":{"updatedTime":1664050274000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":7.57,"words":2272},"filePathRelative":"docs/atom-archive/hacking-atom/sections/hacking-on-atom-core.md"}');export{e as data}; diff --git a/assets/hacking-on-the-core.html.5c30ae63.js b/assets/hacking-on-the-core.html.5c30ae63.js new file mode 100644 index 0000000000..be708ecbc5 --- /dev/null +++ b/assets/hacking-on-the-core.html.5c30ae63.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-143cd798","path":"/docs/launch-manual/sections/core-hacking/sections/hacking-on-the-core.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Hacking on the Core","slug":"hacking-on-the-core","link":"#hacking-on-the-core","children":[{"level":3,"title":"Running in Development Mode","slug":"running-in-development-mode","link":"#running-in-development-mode","children":[]},{"level":3,"title":"Running Pulsar Core Tests Locally","slug":"running-pulsar-core-tests-locally","link":"#running-pulsar-core-tests-locally","children":[]}]}],"git":{"updatedTime":1670466847000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.98,"words":295},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/hacking-on-the-core.md"}');export{e as data}; diff --git a/assets/hacking-on-the-core.html.e53f325e.js b/assets/hacking-on-the-core.html.e53f325e.js new file mode 100644 index 0000000000..985d35efb4 --- /dev/null +++ b/assets/hacking-on-the-core.html.e53f325e.js @@ -0,0 +1,4 @@ +import{_ as s,o as t,c as r,a,b as e,d as l,f as o,r as i}from"./app.0e1565ce.js";const c={},d=o(`You will first want to build and run Pulsar from source.
Once you have a local copy of Pulsar cloned and built, you can then run Pulsar in Development Mode. But first, if you cloned Pulsar to somewhere other than LNX/MAC: ~/github/pulsar
- WIN: %USERPROFILE%\\github\\pulsar
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Pulsar. To run Pulsar in Dev Mode, use the --dev
parameter from the terminal:
$ pulsar --dev <path-to-open>
+
There are a couple benefits of running Pulsar in Dev Mode:
`,6),u=o("ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Pulsar is run using the source code from your local pulsar-edit/pulsar
repository. This means you don't have to rebuild after every change, just restart Pulsar \u{1F44D}~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\dev\\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.In order to run Pulsar Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <path-to-your-local-pulsar-repo>
+$ pulsar --test spec
+
Packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Pulsar package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Pulsar which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Pulsar from deferring activation of your package \u2014\xA0see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Pulsar handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings \u2014\xA0you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Pulsar will not activate your package until it has a URI for it to handle \u2014\xA0it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately \u2014\xA0disabling deferred activation means Pulsar takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Pulsar on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger Pulsar with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Pulsar provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Beginning in Atom 1.23, packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Warning: Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Atom package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Atom which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Atom from deferring activation of your package \u2014\xA0see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Atom handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings \u2014\xA0you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Atom will not activate your package until it has a URI for it to handle \u2014\xA0it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately \u2014\xA0disabling deferred activation means Atom takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Atom on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger atom with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Atom provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Atom includes a feature called "custom file types" which you can use by adding some entries into your config.cson
that look like this:
core:
+ customFileTypes:
+ 'source.ruby': [
+ 'Cheffile'
+ 'this-is-also-ruby'
+ ]
+ 'source.cpp': [
+ 'h'
+ ]
+
To uninstall Atom on macOS, run the following commands from the command line:
rm -rf ~/.atom
+rm -rf /usr/local/bin/atom
+rm -rf /usr/local/bin/apm
+rm -rf /Applications/Atom.app
+rm -rf ~/Library/Preferences/com.github.atom.plist
+rm -rf "~/Library/Application Support/com.github.atom.ShipIt"
+rm -rf "~/Library/Application Support/Atom"
+rm -rf "~/Library/Saved Application State/com.github.atom.savedState"
+rm -rf ~/Library/Caches/com.github.atom
+rm -rf ~/Library/Caches/Atom
+
To use a newline in the result of find and replace, enable the Use Regex
option and use "\\n" in your replacement text. For example, given this text:
hello, world, goodbye
+
If you'd like to replace the ", " with a newline so you end up with this text:
hello
+world
+goodbye
+
In the find and replace settings, enable Use Regex
, enter ", " as the find text, and enter "\\n" as the replace text:
Then click Find All
and finally, click Replace All
.
Atom shows there is a new version available but the version fails to install. You might have an error message showing a permissions error for example:
or it will say downloading but forever loops without restarting or updating.
You need to fix one or more of the following directories:
/Applications/Atom.app/
~/Library/Caches/com.github.atom.ShipIt
~/Library/Application Support/com.github.atom.ShipIt
Do the following:
whoami
And then execute these steps for each directory listed above in order:
stat -f "%Su" [directory]
root
root
then execute: sudo chown -R $(whoami) [directory]
Once you've done the above for both directories, start Atom normally and attempt to update \u{1F44D}
',11),l=[s];function n(c,d){return t(),i("div",null,l)}const p=e(r,[["render",n],["__file","i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this.html.vue"]]);export{p as default}; diff --git a/assets/i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this.html.7f10693d.js b/assets/i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this.html.7f10693d.js new file mode 100644 index 0000000000..0a0a381676 --- /dev/null +++ b/assets/i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this.html.7f10693d.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-50328086","path":"/docs/atom-archive/faq/sections/i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"I am unable to update to the latest version of Atom on macOS. How do I fix this?","slug":"i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this","link":"#i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.56,"words":169},"filePathRelative":"docs/atom-archive/faq/sections/i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this.md"}');export{t as data}; diff --git a/assets/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html.2fe045ab.js b/assets/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html.2fe045ab.js new file mode 100644 index 0000000000..81a92a0f6f --- /dev/null +++ b/assets/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html.2fe045ab.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-52c5c70a","path":"/docs/atom-archive/faq/sections/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"I have a question about a specific Atom community package. Where is the best place to ask it?","slug":"i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it","link":"#i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.4,"words":119},"filePathRelative":"docs/atom-archive/faq/sections/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.md"}');export{e as data}; diff --git a/assets/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html.9271651f.js b/assets/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html.9271651f.js new file mode 100644 index 0000000000..1d9919012d --- /dev/null +++ b/assets/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html.9271651f.js @@ -0,0 +1 @@ +import{_ as o}from"./package-issue-link.51bb6d85.js";import{_ as s,o as i,c as n,a as e,b as t,d as c,r}from"./app.0e1565ce.js";const l={},h=e("h3",{id:"i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it","aria-hidden":"true"},"#"),t(" I have a question about a specific Atom community package. Where is the best place to ask it?")],-1),m=e("p",null,"The best place to get a question answered quickly is probably the Issues list for that specific package. You can find the Issues list for a package by going to that package's page on https://atom.io and clicking the Bugs button:",-1),u=e("p",null,[e("img",{src:o,alt:"Bugs button link"})],-1),p={href:"https://github.com/atom/atom/discussions",target:"_blank",rel:"noopener noreferrer"};function k(_,f){const a=r("ExternalLinkIcon");return i(),n("div",null,[h,m,u,e("p",null,[t("And you can always ask Atom-related questions in the "),e("a",p,[t("official Atom message board"),c(a)]),t(". Someone here may know the answer! It's just with over 3,500 packages (as of early February 2016), the forum members may not know all answers for all packages \u{1F600}")])])}const g=s(l,[["render",k],["__file","i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it.html.vue"]]);export{g as default}; diff --git a/assets/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.html.18a6988d.js b/assets/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.html.18a6988d.js new file mode 100644 index 0000000000..a232baa112 --- /dev/null +++ b/assets/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.html.18a6988d.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-779d7601","path":"/docs/atom-archive/faq/sections/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"I\u2019m getting an error about a \u201Cself-signed certificate\u201D. What do I do?","slug":"i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do","link":"#i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.53,"words":160},"filePathRelative":"docs/atom-archive/faq/sections/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.md"}');export{e as data}; diff --git a/assets/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.html.689f5508.js b/assets/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.html.689f5508.js new file mode 100644 index 0000000000..6749bc1f7d --- /dev/null +++ b/assets/i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do.html.689f5508.js @@ -0,0 +1,2 @@ +import{_ as e,o as t,c as o,f as n}from"./app.0e1565ce.js";const i={},a=n(`This means that there is a proxy between you and our servers where someone (typically your employer) has installed a "self-signed" security certificate in the proxy. A self-signed certificate is one that isn't trusted by anyone but the person who created the certificate. Most security certificates are backed by known, trusted and certified companies. So Atom is warning you that your connection to our servers can be snooped and even hacked by whoever created the self-signed certificate. Since it is self-signed, Atom has no way of knowing who that is.
If you decide that unsecured connections to our servers is acceptable to you, you can use the following instructions.
DANGER
\u{1F6A8} Danger: If you decide that unsecured connections to our servers is acceptable to you, you can use the following command:
apm config set strict-ssl false
+
The best way to tweak the syntax is to wrap your syntax style rules with atom-text-editor
and then prepend every scope with syntax--
. If you want your comments to be blue, for example, you would do the following:
atom-text-editor {
+ .syntax--comment {
+ color: blue;
+ }
+}
+
NOTE: Some older icons from version
2.1.2
are still kept for backwards compatibility.
In the Styleguide under the "Icons" section you'll find all the Octicons that are available.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
Now that we've written a number of packages and themes, let's take minute to take a closer look at some of the ways that Pulsar works in greater depth. Here we'll go into more of a deep dive on individual internal APIs and systems of Pulsar, even looking at some Atom source to see how things are really getting done.
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with \`config.get\`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
The atom.config
database is populated on startup from LNX/MAC: ~/.pulsar/config.cson
- WIN: %USERPROFILE%\\.pulsar\\config.cson
but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and LNX/WIN: Ctrl+Backspace - MAC: Alt+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Pulsar, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.js
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for LNX/WIN: Ctrl+O - MAC: Cmd+O when the selection is inside an editor pane:
But if you click inside the Tree View and press LNX/WIN: Ctrl+O - MAC: Cmd+O , it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.You can add the following to your init.js
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
Or if you are still using the init.coffee
file:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Pulsar to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
Pulsar supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
`,2),en={href:"https://atom.io/docs/api/latest/TextEditor#instance-getRootScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},an=n("code",null,"Editor::getRootScopeDescriptor",-1),tn=n("code",null,'[".source.js"]',-1),on={href:"https://atom.io/docs/api/latest/TextEditor#instance-scopeDescriptorForBufferPosition",target:"_blank",rel:"noopener noreferrer"},pn=n("code",null,"Editor::scopeDescriptorForBufferPosition",-1),cn={href:"https://atom.io/docs/api/latest/Cursor#instance-getScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},ln=n("code",null,"Cursor::getScopeDescriptor",-1),rn=n("code",null,'["source.js", "meta.function.js", "entity.name.function.js"]',-1),un=t(`Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer\u2014the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Pulsar to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Pulsar will only attempt to call deserialize if the two versions match, and otherwise return undefined.
Pulsar contains a number of packages that are Node modules instead of Pulsar packages. If you want to make changes to the Node modules, for instance atom-keymap
, you have to link them into the development environment differently than you would a normal Pulsar package.
Here are the steps to run a local version of a Node module within Pulsar. We're using atom-keymap
as an example:
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Pulsar and do the following:
$ cd <WHERE YOU CLONED THE NODE MODULE>
+$ npm install
+$ cd <WHERE YOU CLONED PULSAR>
+$ pulsar -p rebuild
+$ pulsar --dev .
+
Pulsar packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
Pre-release information
This section is about a feature in pre-release. The information below documents the intended functionality but there is still ongoing work to support these features with stability.
While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
STOP
Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the ppm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if ppm has taken care of things for you.
`,9),fn={class:"custom-container note"},yn=n("p",{class:"custom-container-title"},"Note",-1),wn=n("strong",null,"Note:",-1),_n={href:"https://web.pulsar-edit.dev",target:"_blank",rel:"noopener noreferrer"},xn={href:"https://github.com",target:"_blank",rel:"noopener noreferrer"},jn=t('When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\\d+\\.\\d+\\.\\d+
^v\\d+\\.\\d+\\.\\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
pulsar -p publish --tag tagname
where tagname
must match the name of the tag created in the above step$ pulsar -p unpublish <package-name>
+
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ pulsar -p unpublish <package-name@1.2.3>
+
If you need to rename your package for any reason, you can do so with one simple command \u2013 pulsar -p publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ pulsar -p publish --rename <new-package-name>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
You should now have a better understanding of some of the core Pulsar APIs and systems.
`,6);function Wn(Vn,Un){const a=u("ExternalLinkIcon"),r=u("Tabs");return h(),v("div",null,[b,n("p",null,[s("Subscription methods return "),n("a",f,[y,e(a)]),o("TODO: There is no Pulsar API documented yet so keeping link to Atom until we have this"),s(" objects that can be used to unsubscribe. Note in the example above how we save the subscription to the "),w,s(" instance variable and dispose of it when the view is detached. To group multiple subscriptions together, you can add them all to a "),n("a",_,[x,e(a)]),s(" that you dispose when the view is detached.")]),j,n("p",null,[s("If you're exposing package configuration via specific key paths, you'll want to associate them with a schema in your package's main module. Read more about schemas in the "),n("a",q,[s("Config API documentation"),e(a)]),s(". "),o("TODO: There is no Pulsar API documented yet so keeping link to Atom until we have this")]),S,O,T,e(r,{id:"53",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"behind-pulsar"},{tab0:i(({title:p,value:c,isActive:l})=>[P]),tab1:i(({title:p,value:c,isActive:l})=>[z]),tab2:i(({title:p,value:c,isActive:l})=>[C]),_:1}),A,e(r,{id:"173",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"behind-pulsar"},{tab0:i(({title:p,value:c,isActive:l})=>[I]),tab1:i(({title:p,value:c,isActive:l})=>[E]),tab2:i(({title:p,value:c,isActive:l})=>[D]),_:1}),N,n("p",null,[s("Sometimes the problem isn't mapping the command to a key combination, the problem is that Pulsar doesn't recognize properly what keys you're pressing. This is due to "),n("a",M,[s("some limitations in how Chromium reports keyboard events"),e(a)]),s(". But even this can be customized now.")]),L,n("p",null,[s("If you want to know the "),R,s(" for the keystroke you pressed you can paste the following script to your "),n("a",$,[s("developer tools console"),e(a)]),o("TODO: Convert to Pulsar docs when created")]),W,n("p",null,[n("a",V,[U,e(a)]),s(" accepts a "),o("TODO: There is currently no Pulsar API doc so this is being left for the time being"),Y,s(". If you'd like to set a setting for JavaScript function names, you can give it the JavaScript function name "),H,s(":")]),K,n("p",null,[s("A scope descriptor is an "),n("a",F,[s("Object"),e(a)]),o("TODO: There is currently no Pulsar API doc so this is being left for the time being"),s(" that wraps an "),B,s(" of "),J,s("s. The Array describes a path from the root of the syntax tree to a token including "),G,s(" scope names for the entire path.")]),X,n("p",null,[n("a",Q,[Z,e(a)]),s(" accepts a "),o("TODO: There is currently no Pulsar API doc so this is being left for the time being"),nn,s(". You can get the value for your setting scoped to JavaScript function names via:")]),sn,n("ul",null,[n("li",null,[n("a",en,[an,e(a)]),o("TODO: There is currently no Pulsar API doc so this is being left for the time being"),s(" to get the language's descriptor. For example: "),tn]),n("li",null,[n("a",on,[pn,e(a)]),o("TODO: There is currently no Pulsar API doc so this is being left for the time being"),s(" to get the descriptor at a specific position in the buffer.")]),n("li",null,[n("a",cn,[ln,e(a)]),o("TODO: There is currently no Pulsar API doc so this is being left for the time being"),s(" to get a cursor's descriptor based on position. eg. if the cursor were in the name of the method in our example it would return "),rn])]),un,e(r,{id:"420",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"behind-pulsar"},{tab0:i(({title:p,value:c,isActive:l})=>[dn]),tab1:i(({title:p,value:c,isActive:l})=>[kn]),tab2:i(({title:p,value:c,isActive:l})=>[mn]),_:1}),hn,n("p",null,[s("Similarly, to consume a service, specify one or more "),n("a",vn,[s("version "),gn,e(a)]),s(", each paired with the name of a method on the package's main module:")]),bn,n("div",fn,[yn,n("p",null,[wn,s(" The ppm tool will only publish and "),n("a",_n,[s("https://pulsar-edit.dev"),e(a)]),s(" will only list packages that are hosted on "),n("a",xn,[s("GitHub"),e(a)]),s(", regardless of what process is used to publish them.")])]),jn,n("p",null,[s("Some packages get too big for one person. Sometimes priorities change and someone else wants to help out. You can let others help or create co-owners by "),n("a",qn,[s("adding them as a collaborator"),e(a)]),s(" on the GitHub repository for your package. "),Sn,s(" Anyone that has push access to your repository will have the ability to publish new versions of the package that belongs to that repository.")]),n("p",null,[s("You can also have packages that are owned by a "),n("a",On,[s("GitHub organization"),e(a)]),s(". Anyone who is a member of an organization's "),n("a",Tn,[s("team"),e(a)]),s(" which has push access to the package's repository will be able to publish new versions of the package.")]),Pn,zn,n("p",null,[s("If you want to hand off support of your package to someone else, you can do that by "),n("a",Cn,[s("transferring the package's repository"),e(a)]),s(" to the new owner. Once you do that, they can publish a new version with the updated repository information in the "),An,s(".")]),In,n("p",null,[s("If you no longer want to support your package and cannot find anyone to take it over, you can unpublish your package from "),n("a",En,[s("https://pulsar-edit.dev"),e(a)]),s(". For example, if your package is named "),Dn,s(" then the command you would execute is:")]),Nn,n("p",null,[s("This will remove your package from the "),n("a",Mn,[s("https://pulsar-edit.dev"),e(a)]),s(" package registry. Anyone who has already downloaded a copy of your package will still have it and be able to use it, but it will no longer be available for installation by others.")]),Ln,n("p",null,[s("This will remove just this particular version from the "),n("a",Rn,[s("https://pulsar-edit.dev"),e(a)]),s(" package registry.")]),$n])}const Fn=m(g,[["render",Wn],["__file","index.html.vue"]]);export{Fn as default}; diff --git a/assets/index.html.0618f05b.js b/assets/index.html.0618f05b.js new file mode 100644 index 0000000000..d324dd4e31 --- /dev/null +++ b/assets/index.html.0618f05b.js @@ -0,0 +1 @@ +import{_ as r,o as n,c as i,a,b as e,d as o,r as s}from"./app.0e1565ce.js";const l={},c=a("h1",{id:"ide-java",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#ide-java","aria-hidden":"true"},"#"),e(" IDE-Java")],-1),d=a("p",null,[e("Welcome to the "),a("code",null,"ide-java"),e(" wiki!")],-1),h={href:"https://github.com/atom/ide-java/wiki",target:"_blank",rel:"noopener noreferrer"},_={href:"https://github.com/pulsar-edit/ide-java",target:"_blank",rel:"noopener noreferrer"};function u(f,p){const t=s("ExternalLinkIcon");return n(),i("div",null,[c,d,a("p",null,[e("This wiki contains a single file from the original "),a("a",h,[e("upstream Atom Wiki"),o(t)]),e(". That may mean parts of it are out of date, but otherwise hopefully it is a helpful resource relating to our "),a("a",_,[e("IDE-Java Repo"),o(t)]),e("!")])])}const k=r(l,[["render",u],["__file","index.html.vue"]]);export{k as default}; diff --git a/assets/index.html.06c71ebc.js b/assets/index.html.06c71ebc.js new file mode 100644 index 0000000000..374a861a3c --- /dev/null +++ b/assets/index.html.06c71ebc.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-49f587fe","path":"/docs/packages/core/ide-java/","title":"IDE-Java","lang":"en-us","frontmatter":{"lang":"en-us","title":"IDE-Java"},"excerpt":"","headers":[],"git":{"updatedTime":1664399092000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.21,"words":63},"filePathRelative":"docs/packages/core/ide-java/index.md"}');export{e as data}; diff --git a/assets/index.html.07668541.js b/assets/index.html.07668541.js new file mode 100644 index 0000000000..265386b026 --- /dev/null +++ b/assets/index.html.07668541.js @@ -0,0 +1 @@ +import{_ as r,o as l,c,a as e,b as a,d as t,w as h,f as i,r as n}from"./app.0e1565ce.js";const d={},p=i('STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
Collection of general resources for the rest of the manual.
Below is a list of some useful terms we use with regard to Atom.
A buffer is the text content of a file in Atom. It's basically the same as a file for most descriptions, but it's the version Atom has in memory. For instance, you can change the text of a buffer and it isn't written to its associated file until you save it.
A command is a bit of functionality in Atom that can be triggered by the user either through a keybinding or a menu item.
Docks are collapsible pane containers that attach to the left, right, and bottom sides of the Atom window.
Examples:
A key combination is some combination or sequence of keys that are pressed to perform a task.
Examples:
A key sequence is a special case of a key combination. It is a key combination that consists of keys that must be pressed and released in sequence. Ctrl+K Down is a key sequence. Alt+S is not a key sequence because it is two keys that are pressed and released together rather than in succession.
A keybinding is the mapping of a key combination, such as Ctrl+Enter to an Atom command.
A keymap is a collection of keybindings. It can also refer to a file or files containing keybindings for an Atom package or Atom itself.
A pane is a visual section of the editor space. Each pane can hold multiple pane items. There is always at least one pane in each Atom window.
A section of the Atom UI that can contain multiple panes.
Some item, often an editor, that is displayed within a pane. In the default configuration of Atom, pane items are represented by tabs at the top of each pane.
',6),u={class:"custom-container note"},f=e("p",{class:"custom-container-title"},"Note",-1),b=e("strong",null,"Note:",-1),k={href:"https://github.com/atom/tabs",target:"_blank",rel:"noopener noreferrer"},y=e("h4",{id:"panel",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#panel","aria-hidden":"true"},"#"),a(" Panel")],-1),g=e("p",null,"A piece of the Atom UI that is outside the editor space.",-1),_=e("p",null,"Examples:",-1),x=e("ul",null,[e("li",null,"Find and Replace"),e("li",null,"Keybinding Resolver")],-1);function A(w,v){const o=n("RouterLink"),s=n("ExternalLinkIcon");return l(),c("div",null,[p,e("p",null,[a("An Atom plugin. There is a bunch more information in the section on "),t(o,{to:"/using-atom/sections/atom-packages/"},{default:h(()=>[a("Atom Packages")]),_:1}),a(".")]),m,e("div",u,[f,e("p",null,[b,a(` The reason why we don't call them "tabs" is because you can disable the `),e("a",k,[a("tabs package"),t(s)]),a(" and then there aren't any tabs. For a similar reason, we don't call them files because some things can be shown in a pane that aren't files, like the Settings View.")])]),y,g,_,x])}const E=r(d,[["render",A],["__file","index.html.vue"]]);export{E as default}; diff --git a/assets/index.html.076d2cf9.js b/assets/index.html.076d2cf9.js new file mode 100644 index 0000000000..a968463e84 --- /dev/null +++ b/assets/index.html.076d2cf9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-7412c3f9","path":"/docs/packages/core/atom-languageclient/","title":"Atom-LanguageClient","lang":"en-us","frontmatter":{"lang":"en-us","title":"Atom-LanguageClient","prev":"/docs/packages/core/","next":"/docs/packages/core/list.html"},"excerpt":"","headers":[],"git":{"updatedTime":1664399092000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":4}]},"readingTime":{"minutes":0.24,"words":72},"filePathRelative":"docs/packages/core/atom-languageclient/index.md"}');export{e as data}; diff --git a/assets/index.html.077e976b.js b/assets/index.html.077e976b.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.077e976b.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.08b5d061.js b/assets/index.html.08b5d061.js new file mode 100644 index 0000000000..73c204826b --- /dev/null +++ b/assets/index.html.08b5d061.js @@ -0,0 +1,836 @@ +import{_ as m,a as k,b as g,c as v,d as b,e as f}from"./spec-suite.8f5e854d.js";import{_ as h}from"./dev-tools.1b7b2813.js";import{_ as y,a as w,b as _}from"./theme-side-by-side.33cf8d5d.js";import{_ as x}from"./iconography.bf3cea92.js";import{_ as q,a as P,b as T,c as C,d as I,e as S,f as j,g as A,h as N,i as R}from"./cpu-profile-done.c24435bc.js";import{_ as M,o as L,c as W,a as e,b as n,d as a,w as o,e as i,f as t,r as d}from"./app.0e1565ce.js";const O={},U=e("div",{class:"custom-container warning"},[e("p",{class:"custom-container-title"},"Under Construction"),e("p",null,[n("This document is under construction, please check back soon for updates. Please see "),e("a",{href:"/docs/community"},"our socials"),n(" and feel free to ask for assistance or inquire as to the status of this document.")])],-1),E={href:"https://github.com/pulsar-edit/tree-view",target:"_blank",rel:"noopener noreferrer"},F={href:"https://github.com/pulsar-edit/command-palette",target:"_blank",rel:"noopener noreferrer"},D={href:"https://github.com/pulsar-edit/find-and-replace",target:"_blank",rel:"noopener noreferrer"},Y=t('In this section, we're going to learn how to extend the functionality of Pulsar through writing packages. This will be everything from new user interfaces to new language grammars to new themes. We'll learn this by writing a series of increasingly complex packages together, introducing you to new APIs and tools and techniques as we need them.
First though we will look at how to build the editor itself from source.
If you're looking for an example or specific section using a specific API or feature, you can check below for an index to this section and skip right to it.
If you want to investigate a bug, implement a new feature in Pulsar's core or just want to tinker then you will need to build and run Pulsar from source.
',6),z={href:"https://github.com/pulsar-edit/pulsar",target:"_blank",rel:"noopener noreferrer"},V=e("h3",{id:"requirements-and-dependencies",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#requirements-and-dependencies","aria-hidden":"true"},"#"),n(" Requirements and dependencies")],-1),H=e("p",null,"To build Pulsar you will need to meet some basic requirements:",-1),G={href:"https://github.com/pulsar-edit/pulsar/blob/master/.nvmrc",target:"_blank",rel:"noopener noreferrer"},$={href:"https://github.com/nvm-sh/nvm",target:"_blank",rel:"noopener noreferrer"},B=e("li",null,[n("yarn (enable with "),e("code",null,"corepack enable"),n(")")],-1),X=e("li",null,"Git",-1),J=e("li",null,"Python",-1),K=e("li",null,"C++ toolchain",-1),Z={href:"https://wiki.gnome.org/Projects/Libsecret",target:"_blank",rel:"noopener noreferrer"},Q=e("p",null,"For OS or distribution specific instructions see below:",-1),ee=e("h4",{id:"ubuntu-debian",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#ubuntu-debian","aria-hidden":"true"},"#"),n(" Ubuntu/Debian")],-1),ne=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[e("span",{class:"token comment"},"# Install development packages"),n(` +`),e("span",{class:"token function"},"apt"),n(),e("span",{class:"token function"},"install"),n(` build-essential libxkbfile-dev libsecret-1-dev libx11-dev +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),ae=e("h4",{id:"fedora-rhel",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#fedora-rhel","aria-hidden":"true"},"#"),n(" Fedora/RHEL")],-1),se=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[e("span",{class:"token comment"},"# Install development packages"),n(` +dnf `),e("span",{class:"token parameter variable"},"--assumeyes"),n(),e("span",{class:"token function"},"install"),n(),e("span",{class:"token function"},"make"),n(` gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools libX11-devel libxkbfile-devel nss atk gdk-pixbuf2 gtk3 mesa-dri-drivers +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),te=e("h4",{id:"arch",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#arch","aria-hidden":"true"},"#"),n(" Arch")],-1),oe=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[e("span",{class:"token comment"},"# Install the development packges"),n(` +pacman `),e("span",{class:"token parameter variable"},"-S"),n(` base-devel libxkbfile libsecret libx11 libxcrypt-compat +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),ie=e("h4",{id:"opensuse",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#opensuse","aria-hidden":"true"},"#"),n(" OpenSUSE")],-1),le=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[e("span",{class:"token comment"},"# Install development packages"),n(` +`),e("span",{class:"token function"},"zypper"),n(),e("span",{class:"token keyword"},"in"),n(),e("span",{class:"token parameter variable"},"-t"),n(` pattern devel_basis +`),e("span",{class:"token function"},"zypper"),n(),e("span",{class:"token keyword"},"in"),n(` libX11-devel libxkbfile-devel libsecret-devel +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),re=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,`TODO +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),pe={href:"https://visualstudio.microsoft.com/downloads/",target:"_blank",rel:"noopener noreferrer"},ce=t(`To build the application so you can start hacking on the core you will need to download the source code to your local machine and cd
to the pulsar directory:
git clone https://github.com/pulsar-edit/pulsar.git && cd pulsar
+
nvm install
+corepack enable
+
If Node.js is already installed, run the following to make sure the correct version of Node.js is being used (see requirements):
nvm use
+node -v
+
Run the following to initialize and update the submodules:
git submodule init && git submodule update
+
Now install and build Pulsar & ppm:
yarn install
+yarn build
+yarn build:apm
+
Start Pulsar!
yarn start
+
These instructions will also build ppm
(Pulsar Package Manager) but it will require some additional configuration for use.
The following will allow you to build Pulsar as a stand alone binary or installer. After running you will find your your built application in pulsar/binaries
.
The build script will automatically build for your system's CPU architecture, for example building on an x86_64
CPU will produce binaries for x86_64
, building on arm64
will only produce binaries for arm64
.
It is not possible to "cross-build" for different OSs. For Linux binaries you must build from a Linux machine, macOS binaries must be built from macOS etc. Your OS is detected automatically and the script will build the correct binaries for it.
`,14),ke=e("p",null,[n("By default running "),e("code",null,"yarn dist"),n(" will attempt to create "),e("code",null,"appimage"),n(" (for most Linux distributions), "),e("code",null,"deb"),n(" (for Debian or Ubuntu based distributions) and "),e("code",null,"rpm"),n(" (for Red Hat or Fedora based distributions) binaries but you can select the actual target you want to build by appending the above targets to the command. e.g.:")],-1),ge=e("ul",null,[e("li",null,[e("code",null,"yarn dist appimage")]),e("li",null,[e("code",null,"yarn dist deb")]),e("li",null,[e("code",null,"yarn dist rpm")]),e("li",null,[e("code",null,"yarn dist targz")])],-1),ve=e("p",null,[e("code",null,"yarn dist"),n(" will create a "),e("code",null,"dmg"),n(" installer, there are currently no additional targets for macOS.")],-1),be=e("p",null,"As noted above this builds for your current CPU architecture. i.e. on an Intel Mac this will create Intel binaries, on Apple silicon (M1, M2 etc.) this will create Apple silicon binaries.",-1),fe=e("p",null,[n("By default running "),e("code",null,"yarn dist"),n(" will attempt to create an "),e("code",null,"NSIS"),n(" installer as well as a "),e("code",null,"Portable"),n(" executable which does not require installation. If you only wish to build one then you can specify it by appending the above targets to the command e.g.:")],-1),ye=e("ul",null,[e("li",null,[e("code",null,"yarn dist nsis")]),e("li",null,[e("code",null,"yarn dist portable")])],-1),we=e("h2",{id:"using-ppm-pulsar-package-manager",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#using-ppm-pulsar-package-manager","aria-hidden":"true"},"#"),n(" Using ppm (Pulsar Package Manager)")],-1),_e=e("p",null,[e("code",null,"ppm"),n(" is used for installing and managing Pulsar's packages in much the same way that "),e("code",null,"apm"),n(" did on Atom. However at this point in the project there are a few hoops you have to jump through to get it to work correctly.")],-1),xe=e("p",null,[n("After following the build instructions you will find the "),e("code",null,"ppm"),n(" binary at "),e("code",null,"pulsar/ppm/bin/apm"),n(" but by default Pulsar will be looking in the wrong place. There will also be issues relating to the Electron version which will prevent install from the package backend. To solve this a couple of environmental variables need to be exported.")],-1),qe=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[e("span",{class:"token builtin class-name"},"export"),n(),e("span",{class:"token assign-left variable"},"ATOM_HOME"),e("span",{class:"token operator"},"="),n("/home/"),e("span",{class:"token operator"},"<"),n("user"),e("span",{class:"token operator"},">"),n(`/.pulsar +`),e("span",{class:"token builtin class-name"},"export"),n(),e("span",{class:"token assign-left variable"},"APM_PATH"),e("span",{class:"token operator"},"="),n(`/ppm/bin/apm +`),e("span",{class:"token builtin class-name"},"export"),n(),e("span",{class:"token assign-left variable"},"ATOM_ELECTRON_VERSION"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"12.2"),n(`.3 +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),Pe=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,`TODO +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Te=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[e("span",{class:"token builtin class-name"},"set"),n(),e("span",{class:"token assign-left variable"},"ATOM_HOME"),e("span",{class:"token operator"},"="),n("C:"),e("span",{class:"token punctuation"},"\\"),n("Users"),e("span",{class:"token punctuation"},"\\"),e("span",{class:"token operator"},"<"),n("user"),e("span",{class:"token operator"},">"),e("span",{class:"token punctuation"},"\\"),n(`.pulsar +`),e("span",{class:"token builtin class-name"},"set"),n(),e("span",{class:"token assign-left variable"},"APM_PATH"),e("span",{class:"token operator"},"="),e("span",{class:"token punctuation"},"\\"),n("ppm"),e("span",{class:"token punctuation"},"\\"),n("bin"),e("span",{class:"token punctuation"},"\\"),n(`apm +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),Ce=t(`You can now use the binary to link or install packages.
For example to install the ide-java
package from source:
# clone the repository and cd into it
+git clone https://github.com/pulsar-edit/ide-java
+cd ide-java
+
+# from the directory where you are running pulsar source code
+<pulsar source>/ppm/bin/apm link
+
You will first want to build and run Pulsar from source.
Once you have a local copy of Pulsar cloned and built, you can then run Pulsar in Development Mode. But first, if you cloned Pulsar to somewhere other than LNX/MAC: ~/github/pulsar
- WIN: %USERPROFILE%\\github\\pulsar
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Pulsar. To run Pulsar in Dev Mode, use the --dev
parameter from the terminal:
$ pulsar --dev <path-to-open>
+
There are a couple benefits of running Pulsar in Dev Mode:
`,9),Ie=t("ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Pulsar is run using the source code from your local pulsar-edit/pulsar
repository. This means you don't have to rebuild after every change, just restart Pulsar \u{1F44D}~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\dev\\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.In order to run Pulsar Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <path-to-your-local-pulsar-repo>
+$ pulsar --test spec
+
To begin, there are a few things we'll assume you know, at least to some degree. Since all of Pulsar is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.
`,5),Ne={href:"https://github.com/pulsar-edit/.github/blob/main/project-birth/CONTRIBUTING-DURING-START.md#decaffeination",target:"_blank",rel:"noopener noreferrer"},Re=t(`MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
You can open the init.js
file in an editor from the LNX: Atom > Init Script - MAC: File > Init Script - WIN: Edit > Init Script menu.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.js
file to have Pulsar greet you with an audio beep every time it loads:
atom.beep();
+
atom.commands.add("atom-text-editor", "markdown:paste-as-link", () => {
+ let clipboardText, editor, selection;
+ if (!(editor = atom.workspace.getActiveTextEditor())) {
+ return;
+ }
+ selection = editor.getLastSelection();
+ clipboardText = atom.clipboard.read();
+ return selection.insertText(
+ "[" + selection.getText() + "](" + clipboardText + ")"
+ );
+});
+
Now, reload Pulsar and use the Command Palette to execute the new command, Markdown: Paste As Link
, by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a keybinding for the command.
Let's get started by writing a very simple package and looking at some of the tools needed to develop one effectively. We'll start by writing a package that tells you how many words are in the current buffer and display it in a small modal window.
You can see that Pulsar has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+\u251C\u2500 grammars/
+\u251C\u2500 keymaps/
+\u251C\u2500 lib/
+\u251C\u2500 menus/
+\u251C\u2500 spec/
+\u251C\u2500 snippets/
+\u251C\u2500 styles/
+\u251C\u2500 index.js
+\u2514\u2500 package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Pulsar will default to looking for an index.js
or index.coffee
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Pulsar has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Pulsar's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Pulsar. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
activate(state)
: This optional method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the serialize()
method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).initialize(state)
: This optional method is similar to activate()
but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after your package's deserializers have been called), initialize()
is guaranteed to be called before everything. Use activate()
if you want to be sure that the workspace is ready; use initialize()
if you need to do some setup prior to your deserializers or view providers being invoked.serialize()
: This optional method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's activate
method so you can restore your view to where the user left off.deactivate()
: This optional method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.Ideally, you won't need much in the way of styling. Pulsar provides a standard set of components which define both the colors and UI elements for any package that fits into Pulsar seamlessly. You can view all of Pulsar's UI components by opening the styleguide: open the command palette LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or type LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G
An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Pulsar UI, this means the key combination will work anywhere in the application.
We'll cover more advanced keybinding stuff a bit later in Keymaps in Depth.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your package.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Pulsar text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole package.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Pulsar but optional. The toggle
method is one Pulsar is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Pulsar starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Pulsar workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple package. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the package so other packages could subscribe to these events.
// Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = \`There are \${count} words.\`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Pulsar windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Pulsar being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Pulsar.
Once you've got your test suite written, you can run it by pressing LNX/WIN: Alt+Ctrl+P - MAC: Alt+Cmd+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the pulsar --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
We've now generated, customized and tested our first package for Pulsar. Congratulations! Now let's go ahead and publish it so it's available to the world.
Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
As in Counting Words, we're using atom.workspace.getActiveTextEditor()
to get the object that represents the active text editor. If this convert()
method is called when not focused on a text editor, nothing will happen.
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L
Now open the Command Palette and search for the Ascii Art: Convert
command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Pulsar launch faster by allowing Pulsar to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the Ascii Art: Convert
command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
The Pulsar keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command Update Package Dependencies: Update
from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run Update Package Dependencies: Update
whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(\`\\n\${art}\\n\`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
`,6),$n={href:"https://atom.io/docs/api/latest/TextEditor#instance-getSelectedText",target:"_blank",rel:"noopener noreferrer"},Bn=e("code",null,"editor.getSelectedText()",-1),Xn={href:"https://atom.io/docs/api/latest/TextEditor#instance-insertText",target:"_blank",rel:"noopener noreferrer"},Jn=e("code",null,"editor.insertText()",-1),Kn=t('In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.
We saw in our Word Count package how we could show information in a modal panel. However, panels aren't the only way to extend Pulsar's UI\u2014you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Pulsar will restore them the next time you open the project.
',4),Zn={href:"https://github.com/pulsar-edit/active-editor-info",target:"_blank",rel:"noopener noreferrer"},Qn=e("h3",{id:"create-the-package",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#create-the-package","aria-hidden":"true"},"#"),n(" Create the Package")],-1),ea=e("strong",null,[e("em",null,"LNX/WIN")],-1),na=e("kbd",null,"Ctrl+Shift+P",-1),aa=e("strong",null,[e("em",null,"MAC")],-1),sa=e("kbd",null,"Cmd+Shift+P",-1),ta={href:"https://github.com/pulsar-edit/command-palette",target:"_blank",rel:"noopener noreferrer"},oa=e("code",null,"Package Generator: Generate Package",-1),ia=e("a",{href:"#package-generator"},"the section on package generation",-1),la=e("code",null,"active-editor-info",-1),ra=t(`Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Pulsar. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Pulsar will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Pulsar uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Pulsar for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Pulsar to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the Active Editor Info: Toggle
command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = \`
+ <h2>\${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> \${item.softWrapped}</li>
+ <li><b>Tab Length:</b> \${item.getTabLength()}</li>
+ <li><b>Encoding:</b> \${item.getEncoding()}</li>
+ <li><b>Line Count:</b> \${item.getLineCount()}</li>
+ </ul>
+ \`;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Pulsar how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Pulsar and toggle the view with the Active Editor Info: Toggle
command. Then reload Pulsar again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
Pulsar supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
',5),ua={href:"https://speakerdeck.com/danmatthews/less-css",target:"_blank",rel:"noopener noreferrer"},da=t('package.json
(as covered in Pulsar package.json
). This file is used to help distribute your theme to Pulsar users.package.json
must contain a theme
key with a value of ui
or syntax
for Pulsar to recognize and load it as a theme.Let's create your first theme.
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing Generate Syntax Theme
to generate a new theme package. Select Generate Syntax Theme
, and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Syntax themes should end with -syntax and UI themes should end with -ui.
Pulsar will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Pulsar by pressing LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L to see the changes you made reflected in your Pulsar window. Pretty neat!
Tip
You can avoid reloading to see changes you make by opening an Pulsar window in Dev Mode. To open a Dev Mode Pulsar window run pulsar --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Pulsar's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
',13),ka={href:"https://github.com/pulsar-edit/ui-theme-template",target:"_blank",rel:"noopener noreferrer"},ga=t("pulsar --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
pulsar -p link --dev
to symlink your repository to LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar
Tip
Because we used pulsar -p link --dev
in the above instructions, if you break anything you can always close Pulsar and launch Pulsar normally to force Pulsar to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
',4),ba={href:"https://github.com/pulsar-edit/pulsar/blob/master/static/variables/ui-variables.less",target:"_blank",rel:"noopener noreferrer"},fa={href:"https://github.com/pulsar-edit/pulsar/blob/master/static/variables/syntax-variables.less",target:"_blank",rel:"noopener noreferrer"},ya=e("p",null,"These default values will be used as a fallback in case a theme doesn't define its own variables.",-1),wa=e("h4",{id:"use-in-packages",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#use-in-packages","aria-hidden":"true"},"#"),n(" Use in Packages")],-1),_a=e("p",null,[n("In any of your package's "),e("code",null,".less"),n(" files, you can access the theme variables by importing the "),e("code",null,"ui-variables"),n(" or "),e("code",null,"syntax-variables"),n(" file from Pulsar.")],-1),xa={href:"https://github.com/pulsar-edit/styleguide",target:"_blank",rel:"noopener noreferrer"},qa=t(`Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
To launch a Dev Mode window:
pulsar --dev
If you'd like to reload all the styles at any time, you can use the shortcut LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L,
Pulsar is based on the Chromium browser and supports its Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I
The dev tools allow you to inspect elements and take a look at their CSS properties.
To open the Styleguide, open the command palette with LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or use the shortcut LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes unusable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Pulsar in "normal" mode to fix the issue, it's advised to open two Pulsar windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Pulsar users, it's time to publish it. \u{1F389}
',9),La=e("h2",{id:"creating-a-grammar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#creating-a-grammar","aria-hidden":"true"},"#"),n(" Creating a Grammar")],-1),Wa={href:"http://tree-sitter.github.io/tree-sitter",target:"_blank",rel:"noopener noreferrer"},Oa={href:"https://en.wikipedia.org/wiki/Abstract_syntax_tree",target:"_blank",rel:"noopener noreferrer"},Ua=e("em",null,"syntax trees",-1),Ea=t('This syntax tree gives Pulsar a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Pulsar are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Pulsar: a parser and a grammar file.
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Pulsar package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+\u251C\u2500\u2500 LICENSE
+\u251C\u2500\u2500 README.md
+\u251C\u2500\u2500 grammars
+\u2502 \u2514\u2500\u2500 mylanguage.cson
+\u2514\u2500\u2500 package.json
+
The mylanguage.cson
file specifies how Pulsar should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
Next, the file should contain some fields that indicate to Pulsar when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Pulsar uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
scopes:\n '''\n "*",\n "/",\n "+",\n "-"\n ''': 'keyword.operator'\n
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:\n\n # Apply the classes `syntax--builtin` and `syntax--variable` to all\n # `identifier` nodes whose text is `require`.\n 'identifier': {exact: 'require', scopes: 'builtin.variable'},\n\n # Apply the classes `syntax--type` and `syntax--integer` to all\n # `primitive_type` nodes whose text starts with `int` or `uint`.\n 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},\n\n # Apply the classes `syntax--builtin`, `syntax--class`, and\n # `syntax--name` to `constant` nodes with the text `Array`,\n # `Hash` and `String`. For all other `constant` nodes, just\n # apply the classes `syntax--class` and `syntax--name`.\n 'constant': [\n {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},\n 'class.name'\n ]\n
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector \`call_expression > identifier\` is more
+ # specific than the selector \`identifier\`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
// HTML in a template literal
+const htmlContent = html\`<div>Hello \${name}</div>\`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All \`comment\` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # \`if_statement\` nodes are foldable if they contain an anonymous
+ # "then" token and either an \`elif_clause\` or \`else_clause\` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # \`elif_clause\` or \`else_clause\`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Pulsar's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or uncomment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Pulsar:
`,23),Cs={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-shellscript",target:"_blank",rel:"noopener noreferrer"},Is={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-c",target:"_blank",rel:"noopener noreferrer"},Ss={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-go",target:"_blank",rel:"noopener noreferrer"},js={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-html",target:"_blank",rel:"noopener noreferrer"},As={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-javascript",target:"_blank",rel:"noopener noreferrer"},Ns={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-python",target:"_blank",rel:"noopener noreferrer"},Rs={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-ruby",target:"_blank",rel:"noopener noreferrer"},Ms={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-typescript",target:"_blank",rel:"noopener noreferrer"},Ls=t('Pulsar's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Pulsar's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Pulsar uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Grammar packages should start with language-.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \\bFlight Manual\\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\\\bFlight Manual\\\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
Tip
All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Astute readers may have noticed that the \\b
was changed to \\\\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\\\b(Flight) (Manual)\\\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the {{#note}}
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. {{#note}}
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\\\b(Flight) (Manual)\\\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
`,19),Qs={href:"https://gist.github.com/DamnedScholar/622926bcd222eb1ddc483d12103fd315",target:"_blank",rel:"noopener noreferrer"},et={href:"https://gist.github.com/Aerijo/b8c82d647db783187804e86fa0a604a1",target:"_blank",rel:"noopener noreferrer"},nt={href:"http://www.apeth.com/nonblog/stories/textmatebundle.html",target:"_blank",rel:"noopener noreferrer"},at={href:"https://github.com/kkos/oniguruma/blob/master/doc/RE",target:"_blank",rel:"noopener noreferrer"},st={href:"http://manual.macromates.com/en/language_grammars.html",target:"_blank",rel:"noopener noreferrer"},tt={href:"https://github.com/pulsar-edit/first-mate",target:"_blank",rel:"noopener noreferrer"},ot=e("code",null,"first-mate",-1),it={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-python",target:"_blank",rel:"noopener noreferrer"},lt={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-javascript",target:"_blank",rel:"noopener noreferrer"},rt={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/language-html",target:"_blank",rel:"noopener noreferrer"},pt={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages",target:"_blank",rel:"noopener noreferrer"},ct=e("h2",{id:"converting-from-textmate",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#converting-from-textmate","aria-hidden":"true"},"#"),n(" Converting from TextMate")],-1),ut={href:"https://macromates.com",target:"_blank",rel:"noopener noreferrer"},dt=e("h3",{id:"converting-a-textmate-grammar-bundle",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#converting-a-textmate-grammar-bundle","aria-hidden":"true"},"#"),n(" Converting a TextMate Grammar Bundle")],-1),ht=e("p",null,"Converting a TextMate bundle will allow you to use its editor preferences, snippets, and colorization inside Pulsar.",-1),mt={href:"https://en.wikipedia.org/wiki/R_(programming_language)",target:"_blank",rel:"noopener noreferrer"},kt={href:"https://github.com/textmate",target:"_blank",rel:"noopener noreferrer"},gt=t(`You can convert the R bundle with the following command:
$ pulsar -p init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the pulsar -p link
command, your new package is ready to use. Launch Pulsar and open a .r
file in the editor to see it in action!
The utility that converts the theme first parses the theme's plist file and then creates comparable CSS rules and properties that will style Pulsar similarly.
Download the theme you wish to convert.
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ pulsar -p init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Pulsar and opening the Settings View with the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
package.json
file has name
, description
, and repository
fields.package.json
name
is URL Safe, as in it's not an emoji or special character.package.json
file has a version
field with a value of "0.0.0"
.package.json
file has an engines
field that contains an entry for atom
such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ pulsar -p publish minor
+
Your package is now published and available on Pulsar Package Repository. Head on over to https://web.pulsar-edit.dev/packages/your-package-name
to see your package's page.
With pulsar -p publish
, you can bump the version and publish by using
$ pulsar -p publish <version-type>
+
where version-type
can be major
, minor
and patch
.
i.e. to bump a package from v1.0.0 to v1.1.0:
$ pulsar -p publish minor
+
NOTE: Some older icons from version
2.1.2
are still kept for backwards compatibility.
In the Styleguide under the "Icons" section you'll find all the Octicons that are available.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
You might be running into an issue which was already fixed in a more recent version of Pulsar than the one you're using.
If you're using a released version, check which version of Pulsar you're using:
$ pulsar --version
+> Pulsar : 1.63.0-dev
+> Electron: 12.2.3
+> Chrome : 89.0.4389.128
+> Node : 14.16.0
+
If you're building Pulsar from source, pull down the latest version of master and re-build. Make sure that if logging an issue you include the latest commit hash you built from.
A large part of Pulsar's functionality comes from packages you can install. Pulsar will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Pulsar from the terminal in safe mode:
$ pulsar --safe
+
This starts Pulsar, but does not load packages from LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\packages
or %USERPROFILE%\\.pulsar\\dev\\packages
. and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Pulsar normally still produces the error, then try figuring out which package is causing trouble. Start Pulsar normally again and open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Pulsar or reload Pulsar with LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L. after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Pulsar saves a number of things about your environment when you exit in order to restore Pulsar to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Pulsar from working properly. In these cases, you may want to clear the state that Pulsar has saved.
DANGER
Clearing the saved state permanently destroys any state that Pulsar has saved across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ pulsar --clear-window-state
+
In some cases, you may want to reset Pulsar to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
`,15),co=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[n("$ "),e("span",{class:"token function"},"mv"),n(` ~/.pulsar ~/.pulsar-backup +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),uo=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[n("$ "),e("span",{class:"token function"},"mv"),n(` ~/.pulsar ~/.pulsar-backup +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),ho=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[n("$ "),e("span",{class:"token function"},"rename"),n(" %USERPROFILE%"),e("span",{class:"token punctuation"},"\\"),n(`.pulsar .pulsar-backup +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),mo=t(`Once that is complete, you can launch Pulsar as normal. Everything will be just as if you first installed Pulsar.
Tip
The command given above doesn't delete the old configuration, just puts it somewhere that Pulsar can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the LNX/MAC: ~/.pulsar-backup
- WIN: %USERPROFILE%\\.pulsar-backup
directory.
If you develop or contribute to Pulsar packages, there may be left-over packages linked to your LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\packages
or %USERPROFILE%\\.pulsar\\dev\\packages
. directories. You can use the pulsar -p links
command to list all linked packages:
$ pulsar -p links
+> /Users/pulsy/.pulsar/dev/packages (0)
+> \u2514\u2500\u2500 (no links)
+> /Users/pulsy/.pulsar/packages (1)
+> \u2514\u2500\u2500 color-picker -> /Users/pulsy/github/color-picker
+
You can remove links using the pulsar -p unlink
command:
$ pulsar -p unlink color-picker
+> Unlinking /Users/pulsy/.pulsar/packages/color-picker \u2713
+
See pulsar -p links --help
and pulsar -p unlink --help
for more information on these commands.
Tip
You can also use pulsar -p unlink --all
to easily unlink all packages and themes.
Show the keybinding resolver with LNX/WIN: Ctrl+. - MAC: Cmd+., or with Keybinding Resolver: Show
from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
',5),Yo=e("ul",null,[e("li",null,[e("p",null,"The key combination was not used in the context defined by the keybinding's selector"),e("p",null,[n("For example, you can't trigger the keybinding for the "),e("code",null,"tree-view:add-file"),n(" command if the Tree View is not focused.")])]),e("li",null,[e("p",null,"There is another keybinding that took precedence"),e("p",null,"This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.")])],-1),zo=e("code",null,"keymap.cson",-1),Vo={href:"https://github.com/orgs/pulsar-edit/discussions",target:"_blank",rel:"noopener noreferrer"},Ho=e("h3",{id:"check-font-rendering-issues",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#check-font-rendering-issues","aria-hidden":"true"},"#"),n(" Check Font Rendering Issues")],-1),Go=e("strong",null,[e("em",null,"LNX/WIN")],-1),$o=e("kbd",null,"Ctrl+Shift+I",-1),Bo=e("strong",null,[e("em",null,"MAC")],-1),Xo=e("kbd",null,"Alt+Cmd+I",-1),Jo={href:"https://developers.google.com/web/tools/chrome-devtools/inspect-styles/",target:"_blank",rel:"noopener noreferrer"},Ko=t('When an unexpected error occurs in Pulsar, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
To run a profile, open the Developer Tools with LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Pulsar:
$ pulsar --profile-startup .
+
This will automatically capture a CPU profile as Pulsar is loading and open the Developer Tools once Pulsar loads. From there:
You can then include the startup profile in any issue you report.
If you are having issues installing a package using pulsar -p install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run pulsar -p install --check
to see if the Pulsar package manager can build native code on your machine.
Check out the pre-requisites in the build instructions for your platform for more details.
If you encounter flickering or other rendering issues, you can stop Pulsar from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ pulsar --disable-gpu
+
Chromium (and thus Pulsar) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Pulsar to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage on portable devices as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ pulsar --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Pulsar flags as they will not be executed after the Chromium flags e.g.:
$ pulsar --safe --enable-gpu-rasterization --force-gpu-rasterization
+
We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Pulsar?
`,20),mi={href:"https://jasmine.github.io/archives/1.3/introduction",target:"_blank",rel:"noopener noreferrer"},ki=e("h3",{id:"create-a-new-spec",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#create-a-new-spec","aria-hidden":"true"},"#"),n(" Create a New Spec")],-1),gi={href:"https://github.com/pulsar-edit/pulsar/tree/master/spec",target:"_blank",rel:"noopener noreferrer"},vi={href:"https://github.com/pulsar-edit/markdown-preview/tree/master/spec",target:"_blank",rel:"noopener noreferrer"},bi=e("code",null,"spec",-1),fi=t(`Spec files must end with -spec
so add sample-spec.js
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Pulsar includes the following:
`,3),_i={href:"https://github.com/velesin/jasmine-jquery",target:"_blank",rel:"noopener noreferrer"},xi=t("toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domWriting Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Pulsar. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Pulsar core specs when working on Pulsar itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
To run tests on the command line, run Pulsar with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
pulsar --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Pulsar will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Pulsar will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Pulsar's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.pulsar
).enablePersistence
A boolean indicating whether the Pulsar environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via pulsar --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finished running. This exit code will be returned when running your tests via the command line.
Packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Pulsar package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Pulsar which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Pulsar from deferring activation of your package \u2014\xA0see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Pulsar handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings \u2014\xA0you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Pulsar will not activate your package until it has a URI for it to handle \u2014\xA0it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately \u2014\xA0disabling deferred activation means Pulsar takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Pulsar on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger Pulsar with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Pulsar provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Pulsar runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)/my\\ test
"c:\\my test"
\\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\\
and d:\\
LF
CRLF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
The first step is creating your own clone. For some packages, you may also need to install the requirements necessary for building Pulsar in order to run pulsar -p install
.
For example, if you want to make changes to the tree-view
package, fork the repo on your GitHub account, then clone it:
$ git clone https://github.com/pulsar-edit/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ pulsar -p install
+> Installing modules \u2713
+
Now you can link it to development mode so when you run an Pulsar window with pulsar -p --dev
, you will use your fork instead of the built in package:
$ pulsar -p link -d
+
Editing a package in Pulsar is a bit of a circular experience: you're using Pulsar to modify itself. What happens if you temporarily break something? You don't want the version of Pulsar you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the Application: Open Dev
command. You can also run dev mode from the command line with pulsar --dev
.
To load your package in development mode, create a symlink to it in LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\\.pulsar\\dev\\packages
. This occurs automatically when you clone the package with pulsar -p develop
. You can also run pulsar -p link --dev
and pulsar -p unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running pulsar -p update
after pulling any upstream changes.
Tip
In most cases, we recommend generating a brand new package or a brand new theme as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Pulsar package.
Unzip the file to a temporary location (for example LNX/MAC: /tmp/pulsar
- WIN: C:\\TEMP\\pulsar
)
Copy the contents of the desired package into a working directory for your fork
$ git init
+$ git commit -am "Import core Pulsar package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
The code in the original package will continue to evolve over time, either to fix bugs or to add new enhancements. You may want to incorporate some or all of those updates into your package. To do so, you can follow these steps for merging upstream changes into your package.
Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
$ git remote add upstream https://github.com/pulsar-edit/pulsar.git
+
Tip
Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the pulsar-edit/pulsar repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log --oneline upstream/master -- packages/one-light-ui
+f884f6de8 [themes] Rename A[a]tom -> P[p]ulsar
+0db3190f4 Additional rebranding where needed
+234adb874 Remove deprecated code strings
+...
+
Look through the log and identify the commits that you want to merge into your fork.
For each commit that you want to bring into your fork, use [git format-patch
][https://git-scm.com/docs/git-format-patch] in conjunction with [git am
][https://git-scm.com/docs/git-am]. For example, to merge commit f884f6de8
into your fork:
$ git format-patch -1 --stdout f884f6de8 | git am -p3
+
Repeat this step for each commit that you want to merge into your fork.
If you finished this chapter, you should be an Pulsar-hacking master. We've discussed how you should work with JavaScript and CoffeeScript, and how to put it to good use in creating packages. You should also be able to do this in your own created theme now.
Even when something goes wrong, you should be able to debug this easily. But also fewer things should go wrong, because you are capable of writing great specs for Pulsar.
In the next chapter, we\u2019ll go into more of a deep dive on individual internal APIs and systems of Pulsar, even looking at some Pulsar source to see how things are really getting done.
If you have any issues then please feel free to ask for help from the Pulsar Team or the wider community via any of our Community areas
`,18),ml={href:"https://github.com/pulsar-edit/pulsar/issues",target:"_blank",rel:"noopener noreferrer"},kl={href:"https://github.com/pulsar-edit/pulsar/issues/new?assignees=&labels=bug%2Ctriage&projects=&template=bug-report.yml",target:"_blank",rel:"noopener noreferrer"};function gl(vl,bl){const s=d("ExternalLinkIcon"),u=d("Tabs"),c=d("RouterLink");return L(),W("div",null,[U,e("p",null,[n(`Now it's time to come to the "Hackable" part of the Hyper-Hackable Editor. As we've seen throughout the second section, a huge part of Pulsar is made up of bundled packages. If you wish to add some functionality to Pulsar, you have access to the same APIs and tools that the core features of Pulsar has. From the `),e("a",E,[n("tree-view"),a(s)]),n(" to the "),e("a",F,[n("command-palette"),a(s)]),n(" to "),e("a",D,[n("find-and-replace"),a(s)]),n(" functionality, even the most core features of Pulsar are implemented as packages.")]),Y,e("p",null,[n("The Pulsar application code can be found in the "),e("a",z,[n("pulsar-edit/pulsar"),a(s)]),n(" repository.")]),V,H,e("ul",null,[e("li",null,[n("Node.js (version specified in "),e("a",G,[n("pulsar/.nvmrc"),a(s)]),n(", recommended installation is via "),e("a",$,[n("nvm"),a(s)]),n(")")]),B,X,J,K,e("li",null,[e("a",Z,[n("Libsecret"),a(s)]),n(" development headers")])]),Q,a(u,{id:"182",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"core-hacking"},{tab0:o(({title:l,value:r,isActive:p})=>[ee,ne,ae,se,te,oe,ie,le]),tab1:o(({title:l,value:r,isActive:p})=>[re]),tab2:o(({title:l,value:r,isActive:p})=>[e("p",null,[n("Firstly install "),e("a",pe,[n("Visual Studio"),a(s)]),n(" from Microsoft.")])]),_:1}),ce,e("p",null,[n("Install Node.js (using "),ue,n(" - see above) and enable corepack (for "),de,n("). This will install the version of Node.js specified in "),e("a",he,[n("pulsar/.nvmrc"),a(s)]),n(":")]),me,a(u,{id:"252",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"core-hacking"},{tab0:o(({title:l,value:r,isActive:p})=>[ke,ge]),tab1:o(({title:l,value:r,isActive:p})=>[ve,be]),tab2:o(({title:l,value:r,isActive:p})=>[fe,ye]),_:1}),we,_e,xe,a(u,{id:"317",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"core-hacking"},{tab0:o(({title:l,value:r,isActive:p})=>[qe]),tab1:o(({title:l,value:r,isActive:p})=>[Pe]),tab2:o(({title:l,value:r,isActive:p})=>[Te]),_:1}),Ce,e("ol",null,[Ie,e("li",null,[n("Packages that contain stylesheets, such as syntax themes, will have those stylesheets automatically reloaded by the "),e("a",Se,[n("dev-live-reload"),a(s)]),n(" package. This does not live reload JavaScript or CoffeeScript files \u2014 you'll need to reload the window ("),je,n(") to see changes to those.")])]),Ae,i("This whole section needs to be reworked once decaff properly starts on the core"),e("p",null,[n('While much of Pulsar has been converted to JavaScript, a lot of older code is still implemented in CoffeeScript but the process of "decaffeination" is ongoing, to continue this conversion feel free to read '),e("a",Ne,[n("more"),a(s)]),n(". Additionally, Pulsar's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:")]),Re,e("p",null,[n("We'll go over examples like this in a bit, but this is what the language looks like. Just about everything you can do with CoffeeScript in Pulsar is also doable in JavaScript. You can brush up on CoffeeScript at "),e("a",Me,[n("coffeescript.org"),a(s)]),n(".")]),e("p",null,[n("Less is an even simpler transition from CSS. It adds a number of useful things like variables and functions to CSS. You can learn about Less at "),e("a",Le,[n("lesscss.org"),a(s)]),n(". Our usage of Less won't get too complex in this book however, so as long as you know basic CSS you should be fine.")]),We,e("div",Oe,[Ue,e("p",null,[n("The default init file for Pulsar has been changed from the previous CoffeeScript "),Ee,n(" file used by Atom to JavaScript. The CoffeeScript file will still work but should you wish to reference the specific version of this document for it then you should look at the "),a(c,{to:"/docs/atom-archive/hacking-atom/#the-init-file"},{default:o(()=>[n("Atom Archive")]),_:1}),n(".")])]),e("p",null,[n("When Pulsar finishes loading, it will evaluate "),Fe,n(" in your "),De,n(": "),Ye,n(" - "),ze,n(": "),Ve,n(" directory, giving you a chance to run JavaScript code to make customizations. Code in this file has full access to "),e("a",He,[n("Pulsar's API"),a(s)]),n("."),i("TODO: Replace link when we have the API documented"),n(" If customizations become extensive, consider creating a package, which we will cover in "),Ge,n(".")]),$e,i("TODO: All API links to be updated when it is documented"),e("p",null,[n("Because "),Be,n(" provides access to Pulsar's API, you can use it to implement useful commands without creating a new package or extending an existing one. Here's a command which uses the "),e("a",Xe,[n("Selection API"),a(s)]),n(" and "),e("a",Je,[n("Clipboard API"),a(s)]),n(" to construct a Markdown link from the selected text and the clipboard contents as the URL:")]),Ke,e("p",null,[n("The simplest way to start a package is to use the built-in package generator that ships with Pulsar. As you might expect by now, this generator is itself a separate package implemented in "),e("a",Ze,[n("package-generator"),a(s)]),n(".")]),Qe,e("div",en,[nn,e("p",null,[n("You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on "),e("a",an,[n("pulsar-edit.dev"),a(s)]),n(' (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the '),sn,n(" package name, you "),tn,n(" be safe \u{1F600}")])]),on,e("p",null,[n("Similar to "),e("a",ln,[n("Node modules"),a(s)]),n(", Pulsar packages contain a "),rn,n(' file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.')]),e("p",null,[n("In addition to some of the regular "),e("a",pn,[n("Node "),cn,n(" keys"),a(s)]),n(" available, Pulsar "),un,n(" files have their own additions.")]),dn,e("p",null,[n("Style sheets for your package should be placed in the "),hn,n(" directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or "),e("a",mn,[n("Less"),a(s)]),n(", but Less is recommended.")]),kn,e("p",null,[n("If you "),gn,n(" need special styling, try to keep only structural styles in the package style sheets. If you "),vn,n(" specify colors and sizing, these should be taken from the active theme's "),e("a",bn,[n("ui-variables.less"),a(s)]),n(".")]),fn,e("p",null,[n("Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling "),e("a",yn,[wn,a(s)]),n(". "),i("TODO: Update when API is documented")]),e("p",null,[n("Next we get the number of words by calling "),e("a",_n,[xn,a(s)]),i("TODO: Update when API is documented"),n(" on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.")]),qn,e("p",null,[n("Under the hood, "),e("a",Pn,[n("Jasmine v1.3"),a(s)]),n(" executes your tests, so you can assume that any DSL available there is also available to your package.")]),Tn,e("p",null,[n("Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with "),e("a",Cn,[n("ascii art"),a(s)]),n('. When you run our new command with the word "cool" selected, it will be replaced with:')]),In,Sn,jn,An,e("p",null,[n("To begin, press "),Nn,n(": "),Rn,n(" - "),Mn,n(": "),Ln,n(" to bring up the "),e("a",Wn,[n("Command Palette"),a(s)]),n('. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in '),On,n(". Enter "),Un,n(" as the name of the package.")]),En,e("p",null,[n("Next we insert a string into the current text editor with the "),e("a",Fn,[Dn,a(s)]),i("TODO: Replace with API link once documented)"),n(' method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.')]),Yn,e("p",null,[n("Now we need to convert the selected text to ASCII art. To do this we will use the "),e("a",zn,[n("figlet"),a(s)]),n(" Node module from "),e("a",Vn,[n("npm"),a(s)]),n(". Open "),Hn,n(" and add the latest version of figlet to the dependencies:")]),Gn,e("p",null,[n("There are a couple of new things in this example we should look at quickly. The first is the "),e("a",$n,[Bn,a(s)]),i("TODO: Replace with API link once documented)"),n(" which, as you might guess, returns the text that is currently selected.")]),e("p",null,[n("We then call the Figlet code to convert that into something else and replace the current selection with it with the "),e("a",Xn,[Jn,a(s)]),i("TODO: Replace with API link once documented)"),n(" call.")]),Kn,i("Below was part of this section in the original docs but as Nuclide is retired we either need a new example or remove this entirely"),i(`This system is used by Pulsar's tree view, as well as by third party +packages like [Nuclide](https://nuclide.io) for its console, debugger, outline +view, and diagnostics (linter results).`),e("p",null,[n("For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at "),e("a",Zn,[n("https://github.com/pulsar-edit/active-editor-info"),a(s)]),n(".")]),Qn,e("p",null,[n("To begin, press "),ea,n(": "),na,n(" - "),aa,n(": "),sa,n(" to bring up the "),e("a",ta,[n("Command Palette"),a(s)]),n('. Type "generate package" and select the '),oa,n(" command, just as we did in "),ia,n(". Enter "),la,n(" as the name of the package.")]),ra,e("p",null,[n("Pulsar's interface is rendered using HTML, and it's styled via "),e("a",pa,[n("Less"),a(s)]),n(" which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.")]),ca,e("ul",null,[e("li",null,[n("Less is a superset of CSS, but it has some really handy features like variables. If you aren't familiar with its syntax, take a few minutes to "),e("a",ua,[n("familiarize yourself"),a(s)]),n(".")]),da,e("li",null,[n("You can find existing themes to install or fork in "),e("a",ha,[n("Pulsar Package Repository"),a(s)]),n(". "),i("TODO: Update to a themes URL if we get one on the front end site")])]),ma,e("ol",null,[e("li",null,[n("Fork the "),e("a",ka,[n("ui-theme-template"),a(s)])]),ga]),va,e("ul",null,[e("li",null,[e("a",ba,[n("ui-variables.less"),a(s)])]),e("li",null,[e("a",fa,[n("syntax-variables.less"),a(s)])])]),ya,wa,_a,e("p",null,[n("Your package should generally only specify structural styling, and these should come from "),e("a",xa,[n("the style guide"),a(s)]),n(". Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!")]),qa,e("p",null,[n("Reloading by pressing "),Pa,n(": "),Ta,n(" - "),Ca,n(": "),Ia,n(" after you make changes to your theme is less than ideal. Pulsar supports "),e("a",Sa,[n("live updating"),a(s)]),n(" of styles on Pulsar windows in Dev Mode.")]),ja,e("p",null,[n("Check out Google's "),e("a",Aa,[n("extensive tutorial"),a(s)]),n(" for a short introduction.")]),Na,e("p",null,[n("If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The "),e("a",Ra,[n("Styleguide"),a(s)]),n(" is a page that renders every component Pulsar supports.")]),Ma,e("p",null,[n("Follow the steps on the "),a(c,{to:"/docs/launch-manual/sections/core-hacking/#publishing/"},{default:o(()=>[n("Publishing")]),_:1}),n(" page. The example used is for the Word Count package, but publishing a theme works exactly the same.")]),La,i("This may need to be thoroughly reworked if we change the tree-sitter implementation"),e("p",null,[n("Pulsar's syntax highlighting and code folding system is powered by "),e("a",Wa,[n("Tree-sitter"),a(s)]),n(". Tree-sitter parsers create and maintain full "),e("a",Oa,[Ua,a(s)]),n(" representing your code.")]),Ea,e("p",null,[n("Tree-sitter generates parsers based on "),e("a",Fa,[n("context-free grammars"),a(s)]),n(" that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Pulsar.")]),e("p",null,[n("They can also be developed and tested at the command line, separately from Pulsar. Tree-sitter has "),e("a",Da,[n("its own documentation page"),a(s)]),n(" on how to create these parsers. The "),e("a",Ya,[n("Tree-sitter GitHub organization"),a(s)]),n(" also contains a lot of example parsers that you can learn from, each in its own repository.")]),e("p",null,[n("Once you have created a parser, you need to publish it to "),e("a",za,[n("the NPM registry"),a(s)]),n(" to use it in Pulsar. To do this, make sure you have a "),Va,n(" and "),Ha,n(" in your parser's "),Ga,n(":")]),$a,e("ul",null,[Ba,Xa,e("li",null,[Ja,n(" - The name of the parser node module that will be used for parsing. This string will be passed directly to "),e("a",Ka,[Za,a(s)]),n(" in order to load the parser.")]),Qa]),es,e("p",null,[n("Note that in this selector, we're using the "),e("a",ns,[n("immediate child combinator"),a(s)]),n(" ("),as,n("). Arbitrary descendant selectors without this combinator (for example "),ss,n(", which would match any "),ts,n(" occurring anywhere within a "),os,n(") are currently not supported.")]),is,e("p",null,[n("You can use the "),e("a",ls,[rs,n(" pseudo-class"),a(s)]),n(" to select nodes based on their order within their parent. For example, this example selects "),ps,n(" nodes which are the fourth (zero-indexed) child of a "),cs,n(" node.")]),us,e("p",null,[n("Finally, you can use double-quoted strings in the selectors to select "),ds,n(" tokens in the syntax tree, like "),hs,n(" and "),ms,n(". See "),e("a",ks,[n("the Tree-sitter documentation"),a(s)]),n(" for more information about named vs anonymous tokens.")]),gs,e("p",null,[n("If multiple selectors in the "),vs,n(" object match a node, the node's classes will be decided based on the "),e("a",bs,[n("most specific"),a(s)]),n(" selector. Note that the "),fs,n(" and "),ys,n(" rules do "),ws,n(" affect specificity, so you may need to supply the same "),_s,n(" or "),xs,n(" rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:")]),qs,e("p",null,[n("For example, in JavaScript, "),e("a",Ps,[n("tagged template literals"),a(s)]),n(" sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:")]),Ts,e("ul",null,[e("li",null,[e("a",Cs,[n("Bash"),a(s)])]),e("li",null,[e("a",Is,[n("C"),a(s)])]),e("li",null,[e("a",Ss,[n("Go"),a(s)])]),e("li",null,[e("a",js,[n("HTML"),a(s)])]),e("li",null,[e("a",As,[n("JavaScript"),a(s)])]),e("li",null,[e("a",Ns,[n("Python"),a(s)])]),e("li",null,[e("a",Rs,[n("Ruby"),a(s)])]),e("li",null,[e("a",Ms,[n("TypeScript"),a(s)])])]),Ls,e("p",null,[n("Grammar files are written in the "),e("a",Ws,[n("CSON"),a(s)]),n(" or "),e("a",Os,[n("JSON"),a(s)]),n(" format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.")]),Us,e("p",null,[n("The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the "),Es,n(", "),Fs,n(", "),Ds,n(", and "),Ys,n(" folders. Furthermore, in "),zs,n(", remove the "),Vs,n(" section. Now create a new folder called "),Hs,n(", and inside that a file called "),Gs,n(". This is the main file that we will be working with - start by populating it with a "),e("a",$s,[n("boilerplate template"),a(s)]),n(". Now let's go over what each key means.")]),Bs,e("p",null,[Xs,n(" is where your regex is contained, and "),Js,n(" is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in "),e("a",Ks,[n("Section 12.4 of the TextMate Manual"),a(s)]),n(".")]),Zs,e("ul",null,[e("li",null,[e("a",Qs,[n("DamnedScholar's Gist"),a(s)]),n(". Provides a template of most keys, each with a short comment explaining their function.")]),e("li",null,[e("a",et,[n("Aerijo's Gist"),a(s)]),n(". Another guide that attempts to fully explain making a grammar package for users of all levels.")]),e("li",null,[e("a",nt,[n("http://www.apeth.com/nonblog/stories/textmatebundle.html"),a(s)]),n(". A blog of a programmer's experience writing a grammar package for TextMate.")]),e("li",null,[e("a",at,[n("Oniguruma docs"),a(s)]),n(". The documentation for the regex engine Pulsar uses.")]),e("li",null,[e("a",st,[n("TextMate Section 12"),a(s)]),n(". Pulsar uses the same principles as laid out here, including the list of acceptable scopes.")]),e("li",null,[e("a",tt,[ot,a(s)]),n(". Not necessary to write a grammar, but a good technical reference for what Pulsar is doing behind the scenes.")]),e("li",null,[n("Look at any existing packages, such as the ones for "),e("a",it,[n("Python"),a(s)]),n(", "),e("a",lt,[n("JavaScript"),a(s)]),n(", "),e("a",rt,[n("HTML"),a(s)]),n(", "),e("a",pt,[n("and more"),a(s)]),n(".")])]),i(` (This is left from the original Atom flight-manual) +TODO: +* \`repository\` and including from repository patterns +* Wrap-up +* Implement Package Generator functionality to generate a grammar +* Intermediate + advanced grammar tutorials +`),ct,e("p",null,[n("It's possible that you have themes or grammars from "),e("a",ut,[n("TextMate"),a(s)]),n(" that you like and use and would like to convert to Pulsar. If so, you're in luck because there are tools to help with the conversion.")]),dt,ht,e("p",null,[n("Let's convert the TextMate bundle for the "),e("a",mt,[n("R"),a(s)]),n(" programming language. You can find other existing TextMate bundles "),e("a",kt,[n("on GitHub"),a(s)]),n(".")]),gt,e("p",null,[n("This section will go over how to convert a "),e("a",vt,[n("TextMate"),a(s)]),n(" theme to an Pulsar theme.")]),bt,e("p",null,[n("TextMate themes use "),e("a",ft,[n("plist"),a(s)]),n(" files while Pulsar themes use "),e("a",yt,[n("CSS"),a(s)]),n(" or "),e("a",wt,[n("Less"),a(s)]),n(" to style the UI and syntax in the editor.")]),_t,e("p",null,[n("Pulsar bundles a command line utility called "),xt,n(" which we first used back in "),a(c,{to:"/docs/launch-manual/sections/using-pulsar/#command-line"},{default:o(()=>[n("Command Line")]),_:1}),n(" to search for and install packages via the command line. This is invoked by using the "),qt,n(" command with the "),Pt,n(" or "),Tt,n(" option. The "),Ct,n(" command can also be used to publish Pulsar packages to the public registry and update them.")]),It,St,jt,e("ul",null,[At,e("li",null,[n("Your "),Nt,n(),Rt,n(" field is "),e("a",Mt,[n("Semver V2"),a(s)]),n(" compliant.")]),Lt,e("li",null,[n("Your package is in a Git repository that has been pushed to "),e("a",Wt,[n("GitHub"),a(s)]),n(". Follow "),e("a",Ot,[n("this guide"),a(s)]),n(" if your package isn't already on GitHub.")])]),Ut,e("p",null,[n("Before you publish a package it is a good idea to check ahead of time if a package with the same name has already been published to "),e("a",Et,[n("the Pulsar Package Repository"),a(s)]),n(". You can do that by visiting "),Ft,n(" to see if the package already exists. If it does, update your package's name to something that is available before proceeding.")]),Dt,e("ol",null,[Yt,zt,e("li",null,[n("Creates a new "),e("a",Vt,[n("Git tag"),a(s)]),n(" for the version being published.")]),Ht,Gt]),$t,i(" TODO: Rewrite this Section once Authentication Information is Public "),e("p",null,[n("If this is the first package you are publishing, the "),Bt,n(" command may prompt you for your GitHub username and password. If you have two-factor authentication enabled, use a "),e("a",Xt,[n("personal access token"),a(s)]),n(" in lieu of a password. This is required to publish and you only need to enter this information the first time you publish. The credentials are stored securely in your "),e("a",Jt,[n("keychain"),a(s)]),n(" once you login.")]),Kt,e("p",null,[n("Check out "),e("a",Zt,[n("semantic versioning"),a(s)]),n(" to learn more about best practices for versioning your package releases.")]),Qt,eo,e("p",null,[n("Pulsar comes bundled with the "),e("a",no,[n("Octicons 4.4.0"),a(s)]),n(" icon set. Use them to add icons to your packages.")]),ao,e("p",null,[n("Although icons can make your UI visually appealing, when used without a text label, it can be hard to guess its meaning. In cases where space for a text label is insufficient, consider adding a "),e("a",so,[n("tooltip"),a(s)]),i("TODO: Needs updating once we have the API documented"),n(" that appears on hover. Or a more subtle "),to,n(" attribute would help as well.")]),oo,e("p",null,[n("Pulsar provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when "),e("a",io,[n("submitting issues"),a(s)]),n(":")]),lo,i("The below does not exist yet, uncomment or update when it is"),i("Then check for the [latest released version](https://github.com/pulsar-edit/pulsar/releases/latest)."),e("p",null,[n("You can find the latest releases on the "),e("a",ro,[n("Pulsar Website"),a(s)]),n(", follow the links for either the latest release or Cirrus CI version. Make sure to mention which version when logging an issue.")]),i(` TODO: This section may need re-instating once/if we have auto-updates working again +::: tabs#Updating + +@tab Linux + +To update to the latest version, you can download it from +[the Pulsar website](https://pulsar-edit.dev/) or [the latest release on GitHub](https://github.com/atom/atom/releases/latest) +and follow the [Installation instructions for Pulsar on Linux](../../getting-started/#installing-pulsar). + +@tab macOS + +If there is a more recent release available, you can update to the most recent +release with the auto-update functionality built in to Pulsar and the +[about package](https://github.com/pulsar-edit/pulsar/tree/master/packages/about). +You can open the About View by using the _Pulsar > About_ menu option to see +whether Pulsar is up-to-date, downloading a new update or click the button to +"Restart and Install Update". + +@tab Windows + +If there is a more recent release available, you can update to the most recent +release with the auto-update functionality built in to Pulsar and the +[about package](https://github.com/pulsar-edit/pulsar/tree/master/packages/about). +You can open the About View by using the _Help > About_ menu option to see +whether Pulsar is up-to-date, downloading a new update or click the button to +"Restart and Install Update". + +::: + +`),po,a(u,{id:"2174",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"debugging"},{tab0:o(({title:l,value:r,isActive:p})=>[co]),tab1:o(({title:l,value:r,isActive:p})=>[uo]),tab2:o(({title:l,value:r,isActive:p})=>[ho]),_:1}),mo,e("p",null,[n("If you have packages installed that use native Node modules, when you upgrade to a new version of Pulsar, they might need to be rebuilt. Pulsar detects this and through the "),e("a",ko,[n("incompatible-packages package"),a(s)]),n(" displays an indicator in the status bar when this happens.")]),go,vo,bo,fo,e("p",null,[n("Open Pulsar's "),e("a",yo,[n("Settings View"),a(s)]),n(" with "),wo,n(": "),_o,n(" - "),xo,n(": "),qo,n(", the "),Po,n(": "),To,n(" - "),Co,n(": "),Io,n(" - "),So,n(": "),jo,n(" menu option, or the "),Ao,n(" command from the "),e("a",No,[n("Command Palette"),a(s)]),n(".")]),Ro,e("p",null,[n("Check Pulsar's settings in the Settings View, there's a description of most configuration options in the "),a(c,{to:"/docs/launch-manual/sections/using-pulsar/#configuration-key-reference"},{default:o(()=>[n("Basic Customization section")]),_:1}),n('. For example, if you want Pulsar to hide the invisible symbols representing whitespace characters, disable the "Show Invisibles" option.')]),Mo,e("p",null,[n("Since Pulsar ships with a set of packages and you can also install additional packages yourself, check the list of packages and their settings. For instance, if you'd like to get rid of the vertical line in the middle of the editor, disable the "),e("a",Lo,[n("Wrap Guide package"),a(s)]),n(". And if you don't like it when Pulsar strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the "),e("a",Wo,[n("whitespace package's"),a(s)]),n(" settings.")]),Oo,Uo,e("p",null,[n("You might have defined some custom styles, keymaps or snippets in "),a(c,{to:"/docs/launch-manual/sections/using-pulsar/#basic-customization/"},{default:o(()=>[n("one of your configuration files")]),_:1}),n(". In some situations, these personal hacks might be causing the unexpected behavior you're observing so try clearing those files and restarting Pulsar.")]),Eo,e("p",null,[n("If a command is not executing when you press a key combination or the wrong command is executing, there might be an issue with the keybinding for that combination. Pulsar ships with the "),e("a",Fo,[n("Keybinding Resolver"),a(s)]),n(", a neat package which helps you understand what key Pulsar saw you press and the command that was triggered because of it.")]),Do,e("p",null,[n("If multiple keybindings are matched, Pulsar determines which keybinding will be executed based on the "),a(c,{to:"/docs/launch-manual/sections/behind-pulsar/#specificity-and-cascade-order"},{default:o(()=>[n("specificity of the selectors and the order in which they were loaded")]),_:1}),n(". If the command you wanted to trigger is listed in the Keybinding Resolver, but wasn't the one that was executed, this is normally explained by one of two causes:")]),Yo,e("p",null,[n("Pulsar loads core Pulsar keybindings and package keybindings first, and user-defined keybindings last. Since user-defined keybindings are loaded last, you can use your "),zo,n(" file to tweak the keybindings and sort out problems like these. See the "),a(c,{to:"/docs/launch-manual/sections/behind-pulsar/#keymaps-in-depth"},{default:o(()=>[n("Keymaps in Depth section")]),_:1}),n(" for more information.")]),e("p",null,[n("If you notice that a package's keybindings are taking precedence over core Pulsar keybindings, it might be a good idea to report the issue on that package's GitHub repository. You can contact Pulsar maintainers on "),e("a",Vo,[n("Pulsar's github discussions"),a(s)]),n(".")]),Ho,e("p",null,[n("You can determine which fonts are being used to render a specific piece of text by using the Developer Tools. To open the Developer Tools press "),Go,n(": "),$o,n(" - "),Bo,n(": "),Xo,n('. Once the Developer Tools are open, click the "Elements" tab. Use the '),e("a",Jo,[n("standard tools for finding the element"),a(s)]),n(' containing the text you want to check. Once you have selected the element, you can click the "Computed" tab in the styles pane and scroll to the bottom. The list of fonts being used will be shown there:')]),Ko,a(u,{id:"2351",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"debugging"},{tab0:o(({title:l,value:r,isActive:p})=>[Zo]),tab1:o(({title:l,value:r,isActive:p})=>[Qo,ei,ni]),tab2:o(({title:l,value:r,isActive:p})=>[ai]),_:1}),si,e("p",null,[n("If Pulsar is taking a long time to start, you can use the "),e("a",ti,[n("Timecop package"),a(s)]),n(" to get insight into where Pulsar spends time while loading.")]),oi,ii,li,ri,pi,e("p",null,[n("If you're experiencing performance problems in a particular situation, your "),e("a",ci,[n("Issue reports"),a(s)]),n(" will be more valuable if you include a saved profile from Chrome's CPU profiler that gives some insight into what is slow.")]),ui,e("p",null,[n("To learn more, check out the "),e("a",di,[n("Chrome documentation on CPU profiling"),a(s)]),n(".")]),hi,e("p",null,[n("Pulsar uses "),e("a",mi,[n("Jasmine"),a(s)]),n(" as its spec framework. Any new functionality should have specs to guard against regressions.")]),ki,e("p",null,[e("a",gi,[n("Pulsar specs"),a(s)]),n(" and "),e("a",vi,[n("package specs"),a(s)]),n(" are added to their respective "),bi,n(" directory. The example below creates a spec for Pulsar core.")]),fi,e("p",null,[n("The best way to learn about expectations is to read the "),e("a",yi,[n("Jasmine documentation"),a(s)]),n(" about them. Below is a simple example.")]),wi,e("ul",null,[e("li",null,[e("a",_i,[n("jasmine-jquery"),a(s)])]),xi]),e("p",null,[n("These are defined in "),e("a",qi,[n("spec/spec-helper.js"),a(s)]),n(".")]),Pi,e("p",null,[n("For a more detailed documentation on asynchronous tests please visit the "),e("a",Ti,[n("Jasmine documentation"),a(s)]),n(".")]),Ci,e("p",null,[n("It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the "),e("a",Ii,[n("Travis CI For Your Packages"),a(s)]),n(" and "),e("a",Si,[n("AppVeyor CI For Your Packages"),a(s)]),n(" posts for more details.")]),ji,e("p",null,[n("Now that we've told Pulsar that we want our package to handle URIs beginning with "),Ai,n(" via our "),Ni,n(" method, we need to actually write this method. Pulsar passes two arguments to your URI handler method; the first one is the fully-parsed URI plus query string, "),e("a",Ri,[n("parsed with Node's "),Mi,a(s)]),n(". The second argument is the raw, string URI; this is normally not needed since the first argument gives you structured information about the URI.")]),Li,e("p",null,[n("Creation and deletion operations may take a few milliseconds to complete. If you need to remove many files and folders consider "),e("a",Wi,[n("RimRAF"),a(s)]),n(" which has built-in retry logic for this.")]),Oi,e("p",null,[n("If you discover a bug or issue with an official Pulsar package then feel free to open up the issue in that specific repository instead. When in doubt just open the issue on the "),e("a",Ui,[n("pulsar-edit/pulsar"),a(s)]),n(" repository but be aware that it may get transferred to the proper package's repository.")]),Ei,e("p",null,[n("Several of Pulsar's core packages are maintained in the "),e("a",Fi,[Di,n(" directory of the pulsar-edit/pulsar repository"),a(s)]),n(". If you would like to use one of these packages as a starting point for your own package, please follow the steps below.")]),Yi,i("Could this be made better with GH CLI?"),e("p",null,[n("For the sake of this guide, let's assume that you want to start with the current code in the "),e("a",zi,[n("one-light-ui"),a(s)]),n(' package, make some customizations to it, and publish your new package under the name "one-light-ui-plus".')]),e("ol",null,[e("li",null,[e("p",null,[n("Download the "),e("a",Vi,[n("current contents of the pulsar-edit/pulsar repository as a zip file"),a(s)])])]),Hi]),a(u,{id:"3062",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"core-hacking"},{tab0:o(({title:l,value:r,isActive:p})=>[Gi]),tab1:o(({title:l,value:r,isActive:p})=>[$i]),tab2:o(({title:l,value:r,isActive:p})=>[Bi]),_:1}),Xi,e("ol",Ji,[e("li",null,[e("p",null,[e("a",Ki,[n("Create a public repository on github.com"),a(s)]),n(" for your new package")])]),Zi,Qi]),el,e("p",null,[n("Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team "),e("a",nl,[n("consolidated many core Atom packages"),a(s)]),n(" into the "),e("a",al,[n("atom/atom repository"),a(s)]),n(". For example, the one-light-ui package was originally maintained in the "),e("a",sl,[n("atom/one-light-ui"),a(s)]),n(" repository, but was moved to the "),e("a",tl,[ol,n(" directory"),a(s)]),n(" in the main repository.")]),e("p",null,[n("The Pulsar team has continued this trend and has move even more packages into the core, particularly default language packages. A list of these packages moved can be found in "),e("a",il,[n("this document"),a(s)]),n(".")]),ll,rl,e("p",null,[n("For the sake of this guide, let's assume that you forked the "),e("a",pl,[n("pulsar-edit/one-light-ui"),a(s)]),n(" repository, renamed your fork to "),cl,n(", and made some customizations.")]),ul,e("p",null,[n("Add the "),e("a",dl,[n("pulsar-edit/pulsar repository"),a(s)]),n(" as a git remote:")]),hl,e("p",null,[n("If you think you have found a bug then please have a look through our existing "),e("a",ml,[n("issues"),a(s)]),n(" and if you can't find anything then please "),e("a",kl,[n("create a new bug report"),a(s)]),n(".")])])}const Pl=M(O,[["render",gl],["__file","index.html.vue"]]);export{Pl as default}; diff --git a/assets/index.html.09d5e6bd.js b/assets/index.html.09d5e6bd.js new file mode 100644 index 0000000000..569da0b948 --- /dev/null +++ b/assets/index.html.09d5e6bd.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-033b47ca","path":"/docs/launch-manual/","title":"Launch Manual","lang":"en-US","frontmatter":{"title":"Launch Manual"},"excerpt":"","headers":[{"level":2,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[]},{"level":2,"title":"Using Pulsar","slug":"using-pulsar","link":"#using-pulsar","children":[]},{"level":2,"title":"Hacking the Core","slug":"hacking-the-core","link":"#hacking-the-core","children":[]},{"level":2,"title":"Behind Pulsar","slug":"behind-pulsar","link":"#behind-pulsar","children":[]},{"level":2,"title":"FAQ","slug":"faq","link":"#faq","children":[]}],"git":{"updatedTime":1668992554000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.78,"words":233},"filePathRelative":"docs/launch-manual/index.md"}');export{e as data}; diff --git a/assets/index.html.0f592a40.js b/assets/index.html.0f592a40.js new file mode 100644 index 0000000000..7134df0fa9 --- /dev/null +++ b/assets/index.html.0f592a40.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-7ab1f304","path":"/tag/chocolatey/","title":"chocolatey Tag","lang":"en-US","frontmatter":{"title":"chocolatey Tag","blog":{"type":"category","name":"chocolatey","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.100503fe.js b/assets/index.html.100503fe.js new file mode 100644 index 0000000000..6e57745254 --- /dev/null +++ b/assets/index.html.100503fe.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-4a89825a","path":"/tag/windows/","title":"windows Tag","lang":"en-US","frontmatter":{"title":"windows Tag","blog":{"type":"category","name":"windows","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.10d7d674.js b/assets/index.html.10d7d674.js new file mode 100644 index 0000000000..12aaf0fb30 --- /dev/null +++ b/assets/index.html.10d7d674.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-109540fd","path":"/category/survey/","title":"survey Category","lang":"en-US","frontmatter":{"title":"survey Category","blog":{"type":"category","name":"survey","key":"category"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.10df3af1.js b/assets/index.html.10df3af1.js new file mode 100644 index 0000000000..2fbc2a52ae --- /dev/null +++ b/assets/index.html.10df3af1.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-154dc4c4","path":"/star/","title":"Star","lang":"en-US","frontmatter":{"title":"Star","blog":{"type":"type","key":"star"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{t as data}; diff --git a/assets/index.html.10fdafc2.js b/assets/index.html.10fdafc2.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.10fdafc2.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.11b1f262.js b/assets/index.html.11b1f262.js new file mode 100644 index 0000000000..7ea95be415 --- /dev/null +++ b/assets/index.html.11b1f262.js @@ -0,0 +1,33 @@ +import{_ as m}from"./windows-system-settings.141561e2.js";import{d as f,_ as g,a as b,b as y,c as w,e as v,f as _,g as p}from"./finder.04f876a5.js";import{_ as k,o as x,c as P,a as e,b as t,d as n,w as a,e as h,f as d,r as c}from"./app.0e1565ce.js";const S={},T=e("h1",{id:"getting-started",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#getting-started","aria-hidden":"true"},"#"),t(" Getting Started")],-1),C={class:"custom-container warning"},A=e("p",{class:"custom-container-title"},"Under Construction",-1),F=e("p",null,"This section is all about how to start using Pulsar, such as how to install it and how to use it for basic text editing.",-1),O=e("h2",{id:"why-pulsar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#why-pulsar","aria-hidden":"true"},"#"),t(" Why Pulsar?")],-1),I=d('There are a lot of text editors out there; why should you spend your time learning about and using Pulsar? Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Pulsar to build Pulsar, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Pulsar is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local sub-processes was a non-starter.
For this reason, we didn't build Pulsar as a traditional web application. Instead, Pulsar is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Pulsar window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Pulsar, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Pulsar on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Pulsar as a great replacement for Atom but we can't do it without your support going forward, since we know that we can't achieve our vision for Pulsar alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
',19),z={href:"https://github.com/pulsar-edit",target:"_blank",rel:"noopener noreferrer"},W=e("h2",{id:"installing-pulsar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#installing-pulsar","aria-hidden":"true"},"#"),t(" Installing Pulsar")],-1),N=e("p",null,"To get started with Pulsar, we'll need to get it on your system. This section will go over installing Pulsar on your system, as well as the basics of how to build it from source.",-1),Y={href:"https://pulsar-edit.dev/",target:"_blank",rel:"noopener noreferrer"},j=e("p",null,"Simply select your operating system (if not opened automatically) and architecture (where necessary) and choose the type of download you require.",-1),L=e("p",null,"The button or buttons should be specific to your platform, and the download package should be easily installable. However, let's go over them here in a bit of detail.",-1),V=e("h3",{id:"universal-releases",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#universal-releases","aria-hidden":"true"},"#"),t(" Universal releases")],-1),D=e("p",null,[t("Releases are provided in "),e("code",null,".AppImage"),t(" and "),e("code",null,".tar.gz"),t(' "universal" formats that should work on most distributions.')],-1),E=e("h4",{id:"appimage",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#appimage","aria-hidden":"true"},"#"),t(" AppImage")],-1),q=e("p",null,"Simply run the Pulsar AppImage from your file manager or the terminal:",-1),M=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,`$ ./pulsar_1.100.0_amd64.AppImage +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),U={href:"https://github.com/TheAssassin/AppImageLauncher",target:"_blank",rel:"noopener noreferrer"},B=e("h4",{id:"tar-gz",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#tar-gz","aria-hidden":"true"},"#"),t(" tar.gz")],-1),$=e("p",null,[t("Simply extract and run the "),e("code",null,"pulsar"),t(" binary or integrate it into your system manually.")],-1),R=e("h3",{id:"debian-ubuntu-based-distributions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#debian-ubuntu-based-distributions","aria-hidden":"true"},"#"),t(" Debian/Ubuntu based distributions")],-1),G=e("code",null,".deb",-1),H=e("p",null,"You can install this by opening it in your file manager or via the terminal:",-1),X=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[t("$ "),e("span",{class:"token function"},"sudo"),t(),e("span",{class:"token function"},"apt"),t(),e("span",{class:"token function"},"install"),t(` ./pulsar_1.100.0_amd64.deb +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),J=e("h3",{id:"fedora-rhel-based-distributions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#fedora-rhel-based-distributions","aria-hidden":"true"},"#"),t(" Fedora/RHEL based distributions")],-1),K=e("code",null,".rpm",-1),Q=e("p",null,"You can install this by opening it in your file manager or via the terminal:",-1),Z=e("div",{class:"language-bash ext-sh line-numbers-mode"},[e("pre",{class:"language-bash"},[e("code",null,[e("span",{class:"token comment"},"# On DNF-based distributions"),t(` +$ `),e("span",{class:"token function"},"sudo"),t(" dnf "),e("span",{class:"token function"},"install"),t(),e("span",{class:"token parameter variable"},"-y"),t(` ./pulsar_1.100.0_amd64.rpm + +`),e("span",{class:"token comment"},"# On YUM-based distributions"),t(` +$ `),e("span",{class:"token function"},"sudo"),t(" yum "),e("span",{class:"token function"},"install"),t(),e("span",{class:"token parameter variable"},"-y"),t(` ./pulsar_1.100.0_amd64.rpm +`)])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),ee=e("code",null,".dmg",-1),te=e("code",null,"Pulsar",-1),ae=e("p",null,[t("If you prefer to install from "),e("code",null,".zip"),t(", this is also provided and requires you to extract the file and drag the "),e("code",null,"Pulsar"),t(' application into your "Applications" folder.')],-1),ne={href:"https://en.wikipedia.org/wiki/Portable_application",target:"_blank",rel:"noopener noreferrer"},oe=e("p",null,"We also have Pulsar available in certain package manager repositories. See the relevant section within the downloads for the installation commands.",-1),se=e("p",null,[e("img",{src:m,alt:"Pulsar on Windows"})],-1),ie=e("p",null,[t("The context menu "),e("code",null,"Open with Pulsar"),t(" in File Explorer, and the option to make Pulsar available for file association using "),e("code",null,"Open with..."),t(", is controlled by the System Settings panel as seen above.")],-1),re=e("p",null,[t("With Pulsar open, click on "),e("code",null,"File > Settings"),t(", and then the "),e("code",null,"System"),t(" tab on the left. Check the boxes next to "),e("code",null,"Show in file context menus"),t(", as well as "),e("code",null,"Show in folder context menus"),t(". And you\u2019re all set.")],-1),le=e("h3",{id:"updating-pulsar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#updating-pulsar","aria-hidden":"true"},"#"),t(" Updating Pulsar")],-1),de=e("p",null,"You should consider updating Pulsar periodically for the latest improvements to the software. Additionally, When Pulsar receives hotfixes for security vulnerabilities you will want to update your version of Pulsar as soon as possible.",-1),ue=e("p",null,"If you have installed Pulsar via a package manager then you should use the instructions provided by that package manager for updating your installation.",-1),he=e("h3",{id:"portable-mode",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#portable-mode","aria-hidden":"true"},"#"),t(" Portable Mode")],-1),ce=e("p",null,[t("Pulsar stores configuration and state in a "),e("code",null,".pulsar"),t(" directory usually located in your home directory ("),e("code",null,"%userprofile%"),t(" on Windows). You can however run Pulsar in portable mode where both the app and the configuration are stored together such as on a removable storage device.")],-1),pe=e("p",null,"To setup Pulsar in portable mode download the relevant package and extract it to your removable storage.",-1),me=e("p",null,[t("Download the "),e("code",null,".appimage"),t(" or "),e("code",null,".tar.gz"),t(" release then create a "),e("code",null,".pulsar"),t(" directory alongside the directory that contains the Pulsar binary, for example:")],-1),fe=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`/media/myusb/pulsar-1.100.0/.pulsar +/media/myusb/.pulsar +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),ge=e("p",null,[t("Download the "),e("code",null,".zip"),t(" release then create a "),e("code",null,".pulsar"),t(" directory alongside the Pulsar.app application, for example:")],-1),be=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`/MyUSB/Pulsar.app +/MyUSB/.pulsar +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),ye=e("p",null,[t("Download the "),e("code",null,"Portable"),t(" release then create a "),e("code",null,".pulsar"),t(" directory alongside the directory that contains pulsar.exe, for example:")],-1),we=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`e:\\pulsar-1.100.0\\pulsar.exe +e:\\.pulsar +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),ve=d('.pulsar
directory must be writeable.pulsar
directory to your portable device.pulsar
directory - just create a subdirectory called electronUserData
inside .pulsar
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ pulsar -p config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure ppm
to use it by running:
$ pulsar -p config set https-proxy <YOUR_PROXY_ADDRESS>
+
You can run pulsar -p config get https-proxy
to verify it has been set correctly.
Now that Pulsar is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Pulsar for the first time, you should get a screen that looks like this:
This is the Pulsar welcome screen and gives you a pretty good starting point for how to get started with the editor.
In that welcome screen, we are introduced to probably the most important command in Pulsar, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like LNX: Ctrl+Shift+P - MAC: Cmd+Shift+P - WIN: Ctrl+Shift+P to demonstrate how to run a command and tabbed sections where necessary where instructions for different platforms may differ.
If you have customized your Pulsar keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Pulsar. Instead of clicking around all the application menus to look for something, you can press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut key strokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Pulsar has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Pulsar Packages.
To open the Settings View, you can:
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Pulsar. Pulsar ships with 4 different UI themes, dark and light variants of the Pulsar and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
',17),xe={href:"https://web.pulsar-edit.dev",target:"_blank",rel:"noopener noreferrer"},Pe=e("h4",{id:"soft-wrap",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#soft-wrap","aria-hidden":"true"},"#"),t(" Soft Wrap")],-1),Se=e("p",null,"You can use the Settings View to specify your whitespace and wrapping preferences.",-1),Te=e("p",null,[e("img",{src:f,alt:"Whitespace and wrapping preferences settings"})],-1),Ce=e("p",null,[t('Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the '),e("kbd",null,"Tab"),t(' key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.')],-1),Ae=e("p",null,'The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.',-1),Fe=d('Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Pulsar. You can do it by choosing File > Open from the menu bar or by pressing
LNX/WIN: Ctrl+O - MAC: Cmd+O
to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
Another way to open a file in Pulsar is from the command line using the pulsar
command.
You can run the pulsar
command with one or more file paths to open up those files in Pulsar.
$ pulsar --help
+> Pulsar Editor v1.100.0
+
+> Usage: pulsar [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Pulsar window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off pulsar [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+getting-started/sections/pulsar-basics.md:130:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ pulsar getting-started/sections/pulsar-basics.md:130
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+getting-started/sections/pulsar-basics.md.md:150:722
+$ pulsar getting-started/sections/pulsar-basics.md:150:722
+
You can open any number of directories from the command line by passing their paths to the pulsar
command line tool. For example, you could run the command pulsar ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Pulsar with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
',4),$e=e("p",null,[t("You can also hide and show it with "),e("kbd",null,"Ctrl+\\"),t(" or the "),e("code",null,"tree-view: toggle"),t(" command from the Command Palette, and "),e("kbd",null,"Alt+\\"),t(" will focus it. When the Tree view has focus you can press "),e("kbd",null,"A"),t(", "),e("kbd",null,"M"),t(", or "),e("kbd",null,"Delete"),t(" to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in your native filesystem or copying the file path to the clipboard.")],-1),Re=e("p",null,[t("You can also hide and show it with "),e("kbd",null,"Cmd+\\"),t(" or the "),e("code",null,"tree-view: toggle"),t(" command from the Command Palette, and "),e("kbd",null,"Ctrl+0"),t(" will focus it. When the Tree view has focus you can press "),e("kbd",null,"A"),t(", "),e("kbd",null,"M"),t(", or "),e("kbd",null,"Delete"),t(" to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in Finder or copying the file path to the clipboard.")],-1),Ge=e("p",null,[t("You can also hide and show it with "),e("kbd",null,"Ctrl+\\"),t(" or the "),e("code",null,"tree-view: toggle"),t(" command from the Command Palette, and "),e("kbd",null,"Alt+\\"),t(" will focus it. When the Tree view has focus you can press "),e("kbd",null,"A"),t(", "),e("kbd",null,"M"),t(", or "),e("kbd",null,"Delete"),t(" to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in Windows Explorer or copying the file path to the clipboard.")],-1),He=e("div",{class:"custom-container note"},[e("p",{class:"custom-container-title"},"Note"),e("p",null,[e("strong",null,"Pulsar Packages")]),e("p",null,"Like many parts of Pulsar, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Pulsar by default. Packages that are bundled with Pulsar are referred to as Core packages. Ones that aren't bundled with Pulsar are referred to as Community packages."),e("p",null,"You can find the source code to the Tree View on GitHub at https://github.com/pulsar-edit/tree-view."),e("p",null,"This is one of the interesting things about Pulsar. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.")],-1),Xe=e("h4",{id:"opening-a-file-in-a-project",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#opening-a-file-in-a-project","aria-hidden":"true"},"#"),t(" Opening a File in a Project")],-1),Je=e("p",null,"Once you have a project open in Pulsar, you can easily find and open any file within that project.",-1),Ke=e("p",null,[t("If you press "),e("kbd",null,"Ctrl+T"),t(" or "),e("kbd",null,"Ctrl+P"),t(", the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.")],-1),Qe=e("p",null,[e("img",{src:p,alt:"Opening files with the Fuzzy Finder",title:"Opening files with the Fuzzy Finder"})],-1),Ze=e("p",null,[t("You can also search through only the files currently opened (rather than every file in your project) with "),e("kbd",null,"Ctrl+B"),t('. This searches through your "buffers" or open files. You can also limit this fuzzy search with '),e("kbd",null,"Ctrl+Shift+B"),t(", which searches only through the files which are new or have been modified since your last Git commit.")],-1),et=e("p",null,[t("If you press "),e("kbd",null,"Cmd+T"),t(" or "),e("kbd",null,"Cmd+P"),t(", the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.")],-1),tt=e("p",null,[e("img",{src:p,alt:"Opening files with the Fuzzy Finder",title:"Opening files with the Fuzzy Finder"})],-1),at=e("p",null,[t("You can also search through only the files currently opened (rather than every file in your project) with "),e("kbd",null,"Cmd+B"),t('. This searches through your "buffers" or open files. You can also limit this fuzzy search with '),e("kbd",null,"Cmd+Shift+B"),t(", which searches only through the files which are new or have been modified since your last Git commit.")],-1),nt=e("p",null,[t("If you press "),e("kbd",null,"Ctrl+T"),t(" or "),e("kbd",null,"Ctrl+P"),t(", the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.")],-1),ot=e("p",null,[e("img",{src:p,alt:"Opening files with the Fuzzy Finder",title:"Opening files with the Fuzzy Finder"})],-1),st=e("p",null,[t("You can also search through only the files currently opened (rather than every file in your project) with "),e("kbd",null,"Ctrl+B"),t('. This searches through your "buffers" or open files. You can also limit this fuzzy search with '),e("kbd",null,"Ctrl+Shift+B"),t(", which searches only through the files which are new or have been modified since your last Git commit.")],-1),it=e("code",null,"core.ignoredNames",-1),rt=e("code",null,"fuzzy-finder.ignoredNames",-1),lt=e("code",null,"core.excludeVCSIgnoredPaths",-1),dt={href:"https://git-scm.com/docs/gitignore",target:"_blank",rel:"noopener noreferrer"},ut=e("code",null,".gitignore",-1),ht=e("code",null,"core.ignoredNames",-1),ct=e("code",null,"fuzzy-finder.ignoredNames",-1),pt={href:"https://github.com/isaacs/minimatch",target:"_blank",rel:"noopener noreferrer"},mt=d('Tip
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
You should now have a basic understanding of what Pulsar is and what you want to do with it. You should also have it installed on your system and be able to use it for the most basic text editing operations.
Now you're ready to start digging into the fun stuff.
',4);function ft(gt,bt){const r=c("RouterLink"),l=c("ExternalLinkIcon"),u=c("Tabs");return x(),P("div",null,[T,e("div",C,[A,e("p",null,[t("This document is under construction, please check back soon for updates. Please see "),n(r,{to:"/community/"},{default:a(()=>[t("our community links")]),_:1}),t(" and feel free to ask for assistance or inquire as to the status of this document.")])]),F,O,h('TODO: We probably want to make this our own, it is very "Atom" still'),I,e("p",null,[t("The entire Pulsar editor is free and open source and available under our "),e("a",z,[t("Organizational"),n(l)]),t(" repositories.")]),W,h("TODO: We need a section here somewhere about how to get pulsar onto the PATH for all systems as it seems to be broken on mac and windows. Also needed for tar.gz and appimage"),N,e("p",null,[t("Installing Pulsar should be fairly simple. Generally, you can go to "),e("a",Y,[t("pulsar-edit.dev"),n(l)]),t(" and you should see a download button.")]),j,L,n(u,{id:"94",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"getting-started"},{tab0:a(({title:o,value:s,isActive:i})=>[h("TODO: When/if we introduce other repository based downloads then we can re-instate much of this from the archived atom section"),V,D,E,q,M,e("p",null,[t("For deeper integration into the system consider using "),e("a",U,[t("AppImageLauncher"),n(l)]),t(".")]),B,$,R,e("p",null,[t("To install Pulsar on Debian, Ubuntu, or related distributions download the "),G,t(" from either the latest release or beta/dev version from "),n(r,{to:"/download.html"},{default:a(()=>[t("Pulsar Downloads")]),_:1}),t(".")]),H,X,J,e("p",null,[t("To install Pulsar on Fedora, RHEL, or related distributions download the "),K,t(" from either the latest release or beta/dev version from "),n(r,{to:"/download.html"},{default:a(()=>[t("Pulsar Downloads")]),_:1}),t(".")]),Q,Z]),tab1:a(({title:o,value:s,isActive:i})=>[e("p",null,[t("Pulsar follows the standard macOS installation process. Grab the correct download "),ee,t(" for your system from "),n(r,{to:"/download.html"},{default:a(()=>[t("Pulsar Downloads")]),_:1}),t(". Once you have the file, you can open it to run the installer and drag the new "),te,t(' application into your "Applications" folder.')]),ae]),tab2:a(({title:o,value:s,isActive:i})=>[e("p",null,[t("Pulsar is available as a 64-bit Windows installer and "),e("a",ne,[t("portable app"),n(l)]),t(" that can be downloaded from "),n(r,{to:"/download.html"},{default:a(()=>[t("Pulsar Downloads")]),_:1}),t(".")]),oe,se,ie,re]),_:1}),le,de,e("p",null,[t("Currently Pulsar does not support automatic updates. What this means is that new versions will have to be obtained via the "),n(r,{to:"/download.html"},{default:a(()=>[t("Pulsar Downloads")]),_:1}),t(" here on our website. This is something on our roadmap to change as soon as possible.")]),ue,h("TODO: Auto upgrade instructions - selectively pull info from atom archive as this becomes possible"),he,ce,pe,n(u,{id:"188",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"getting-started"},{tab0:a(({title:o,value:s,isActive:i})=>[me,fe]),tab1:a(({title:o,value:s,isActive:i})=>[ge,be]),tab2:a(({title:o,value:s,isActive:i})=>[ye,we]),_:1}),ve,e("p",null,[t("The "),n(r,{to:"/docs/launch-manual/sections/core-hacking/#building-pulsar"},{default:a(()=>[t("Hacking the Core")]),_:1}),t(" section of the launch manual covers instructions on how to clone and build the source code if you prefer that option.")]),_e,e("p",null,[t("You can find definitions for all the various terms that we use throughout the manual in our "),n(r,{to:"/docs/launch-manual/sections/getting-started/resources/glossary/"},{default:a(()=>[t("Glossary")]),_:1}),t(".")]),ke,e("p",null,[t("There are also dozens of themes on the "),e("a",xe,[t("Pulsar Package Repository"),n(l)]),t(" that you can choose from if you want something different. We will cover customizing a theme in "),n(r,{to:"/docs/launch-manual/sections/using-pulsar/#basic-customization"},{default:a(()=>[t("Style Tweaks")]),_:1}),t(" and creating your own theme in "),n(r,{to:"/docs/launch-manual/sections/core-hacking/#creating-a-theme"},{default:a(()=>[t("Creating a Theme")]),_:1}),t(".")]),Pe,Se,Te,Ce,Ae,e("p",null,[t("In "),n(r,{to:"/docs/launch-manual/sections/using-pulsar/#basic-customization"},{default:a(()=>[t("Basic Customization")]),_:1}),t(" we will see how to set different wrap preferences for different types of files (for example, if you want to wrap Markdown files but not other files).")]),Fe,e("div",Oe,[Ie,ze,We,e("p",null,[t("On Linux commands are installed automatically as a part of Pulsar's installation process. Windows requires the path to be "),e("a",Ne,[t("exposed manually"),n(l)]),t(" by the user at this time.")])]),Ye,e("p",null,[t("Editing a file is pretty straightforward. You can click around and scroll with your mouse and type to change the content. There is no special editing mode or key commands. If you prefer editors with modes or more complex key commands, you should take a look at the "),e("a",je,[t("Pulsar Package Repository"),n(l)]),t(". There are a lot of packages that emulate popular styles.")]),n(u,{id:"438",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"getting-started"},{tab0:a(({title:o,value:s,isActive:i})=>[Le]),tab1:a(({title:o,value:s,isActive:i})=>[Ve]),tab2:a(({title:o,value:s,isActive:i})=>[De]),_:1}),Ee,n(u,{id:"458",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"getting-started"},{tab0:a(({title:o,value:s,isActive:i})=>[qe]),tab1:a(({title:o,value:s,isActive:i})=>[Me]),tab2:a(({title:o,value:s,isActive:i})=>[Ue]),_:1}),Be,n(u,{id:"487",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"getting-started"},{tab0:a(({title:o,value:s,isActive:i})=>[$e]),tab1:a(({title:o,value:s,isActive:i})=>[Re]),tab2:a(({title:o,value:s,isActive:i})=>[Ge]),_:1}),He,Xe,Je,n(u,{id:"524",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"getting-started"},{tab0:a(({title:o,value:s,isActive:i})=>[Ke,Qe,Ze]),tab1:a(({title:o,value:s,isActive:i})=>[et,tt,at]),tab2:a(({title:o,value:s,isActive:i})=>[nt,ot,st]),_:1}),e("p",null,[t("The fuzzy finder uses the "),it,t(", "),rt,t(" and "),lt,t(" configuration settings to filter out files and folders that will not be shown. If you have a project with tons of files you don't want it to search through, you can add patterns or paths to either of these config settings or your "),e("a",dt,[t("standard "),ut,t(" files"),n(l)]),t(". We'll learn more about config settings in "),n(r,{to:"/docs/launch-manual/sections/using-pulsar/#global-configuration-settings"},{default:a(()=>[t("Global Configuration Settings")]),_:1}),t(", but for now you can easily set these in the Settings View under Core Settings.")]),e("p",null,[t("Both "),ht,t(" and "),ct,t(" are interpreted as glob patterns as implemented by the "),e("a",pt,[t("minimatch Node module"),n(l)]),t(".")]),mt])}const _t=k(S,[["render",ft],["__file","index.html.vue"]]);export{_t as default}; diff --git a/assets/index.html.130de9b2.js b/assets/index.html.130de9b2.js new file mode 100644 index 0000000000..0d45f54dc1 --- /dev/null +++ b/assets/index.html.130de9b2.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-28a80d22","path":"/tag/joke/","title":"joke Tag","lang":"en-US","frontmatter":{"title":"joke Tag","blog":{"type":"category","name":"joke","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.151d8d55.js b/assets/index.html.151d8d55.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.151d8d55.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.16e14736.js b/assets/index.html.16e14736.js new file mode 100644 index 0000000000..44645ffcda --- /dev/null +++ b/assets/index.html.16e14736.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-f5401b6a","path":"/tag/sunset/","title":"sunset Tag","lang":"en-US","frontmatter":{"title":"sunset Tag","blog":{"type":"category","name":"sunset","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.1725108a.js b/assets/index.html.1725108a.js new file mode 100644 index 0000000000..40f18cc77a --- /dev/null +++ b/assets/index.html.1725108a.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5bc93818","path":"/category/","title":"Category","lang":"en-US","frontmatter":{"title":"Category","blog":{"type":"category","key":"category"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.19e04ff4.js b/assets/index.html.19e04ff4.js new file mode 100644 index 0000000000..8b9462d0b4 --- /dev/null +++ b/assets/index.html.19e04ff4.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5b38f390","path":"/tag/modernization/","title":"modernization Tag","lang":"en-US","frontmatter":{"title":"modernization Tag","blog":{"type":"category","name":"modernization","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.1b0cb753.js b/assets/index.html.1b0cb753.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.1b0cb753.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.1cce4ebf.js b/assets/index.html.1cce4ebf.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.1cce4ebf.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.22737491.js b/assets/index.html.22737491.js new file mode 100644 index 0000000000..e5842d62e5 --- /dev/null +++ b/assets/index.html.22737491.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-634d92c5","path":"/docs/atom-archive/getting-started/","title":"Chapter 1 : Getting started","lang":"en-US","frontmatter":{"title":"Chapter 1 : Getting started","sitemap":{"priority":0.1}},"excerpt":"","headers":[{"level":2,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[{"level":3,"title":"Why Atom?","slug":"why-atom","link":"#why-atom","children":[]},{"level":3,"title":"Installing Atom","slug":"installing-atom","link":"#installing-atom","children":[]},{"level":3,"title":"Summary","slug":"summary","link":"#summary","children":[]}]}],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":4},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":0.29,"words":86},"filePathRelative":"docs/atom-archive/getting-started/index.md"}');export{t as data}; diff --git a/assets/index.html.234bb87b.js b/assets/index.html.234bb87b.js new file mode 100644 index 0000000000..df66a418b7 --- /dev/null +++ b/assets/index.html.234bb87b.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-007c0ae2","path":"/tag/video/","title":"video Tag","lang":"en-US","frontmatter":{"title":"video Tag","blog":{"type":"category","name":"video","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.2f30f8af.js b/assets/index.html.2f30f8af.js new file mode 100644 index 0000000000..410b116741 --- /dev/null +++ b/assets/index.html.2f30f8af.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-33e16b10","path":"/tag/update/","title":"update Tag","lang":"en-US","frontmatter":{"title":"update Tag","blog":{"type":"category","name":"update","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.30ea87d2.js b/assets/index.html.30ea87d2.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.30ea87d2.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.320bab91.js b/assets/index.html.320bab91.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.320bab91.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.35187d4a.js b/assets/index.html.35187d4a.js new file mode 100644 index 0000000000..23a1de7e61 --- /dev/null +++ b/assets/index.html.35187d4a.js @@ -0,0 +1,2 @@ +import{_ as n,o,c as r,e as s,a as i,b as e,d as a,f as c,r as d}from"./app.0e1565ce.js";const l={},p=c('In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment include:
Examples of unacceptable behavior by participants include:
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
The only individuals that have access to the above details are Pulsar's Core Admin Team in charge of the Pulsar Backend.
The logs that contain the above information is kept in the cloud for 30 days before it is automatically deleted.
When you create a user account with Pulsar to star, or publish packages, you provide some details to Pulsar to create the account. It should be made explicit that Pulsar does not store any type of credentials your account at any time.
node_id
(Think of your node_id
like the random number assigned to you when making a user account. This is public information)When you sign up with Pulsar, and choose to make an account, this is the data collected to make sure your account works, and is able to protect Packages from Malicious Actors pretending to be someone they're not.
This information is used to let you authenticate against the Pulsar Backend when publishing, deleted, or starring a package.
The only people who have access to this information are Pulsar's Core Admin Team in charge of the Backend.
The above details will be kept until either you request deletion of your account, or there is a built in supported method to delete your Pulsar User Account.
node_id
.https://pulsar-edit.dev
)The Pulsar Website is a service you connect to anytime you visit our website.
',4),C={href:"https://pages.github.com/",target:"_blank",rel:"noopener noreferrer"},W=e("h3",{id:"tldr-2",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#tldr-2","aria-hidden":"true"},"#"),t(" TLDR")],-1),D=e("ul",null,[e("li",null,[t("The Pulsar Website Collects "),e("strong",null,"Zero"),t(" Data about you, or where you are.")]),e("li",null,"It does not Log anything, save anything, or extract anything."),e("li",null,"It's just a basic website.")],-1),H=e("h2",{id:"pulsar-editor",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#pulsar-editor","aria-hidden":"true"},"#"),t(" Pulsar Editor")],-1),L={href:"https://github.com/pulsar-edit/pulsar",target:"_blank",rel:"noopener noreferrer"},U={href:"https://atom.io",target:"_blank",rel:"noopener noreferrer"},E={href:"https://github.com/pulsar-edit/pulsar/pull/40",target:"_blank",rel:"noopener noreferrer"},G=e("p",null,[t("That is to say now, the Pulsar Editor collects "),e("strong",null,"Zero"),t(" information about its users.")],-1),R=e("h3",{id:"tldr-3",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#tldr-3","aria-hidden":"true"},"#"),t(" TLDR")],-1),q=e("li",null,[t("The Pulsar Editor Collects "),e("strong",null,"Zero"),t(" Data about you, or where you are.")],-1),V=e("li",null,"It does not log anything, send anything to remote servers, or extract anything.",-1),O={href:"https://github.com/pulsar-edit/pulsar/pull/40",target:"_blank",rel:"noopener noreferrer"};function N(S,Y){const a=l("ExternalLinkIcon");return s(),n("div",null,[h(`This page is generated from the PRIVACY_POLICY.md page on the org-level +documentation at https://github.com/pulsar-edit/.github`),c,u,p,m,f,b,e("p",null,[t("The Pulsar Backend is the service you connect to when using all Pulsar Packages on the web via "),g,t(" and when using the built in Pulsar Settings View or "),e("a",y,[t("Package Manager"),o(a)]),t(" to interact with community packages.")]),w,_,P,v,k,e("p",null,[t("When you send a request of any kind to the Pulsar Backend this information is made available to us through the web request itself and the "),e("a",T,[t("User Agent"),o(a)]),t(" of the request.")]),x,e("p",null,[t("To request removal of the above details feel free to contact the Pulsar Core Admin Team, for quicker response times, contact use through the "),e("a",A,[t("Pulsar Discord"),o(a)]),t(". Or you can contact us through any of the "),e("a",I,[t("supported methods"),o(a)]),t(", or even via email to admin@pulsar-edit.dev")]),B,e("p",null,[t("This Website is hosted on "),e("a",C,[t("GitHub Pages"),o(a)]),t(" which while conveinent for us, also means that we have no built in mechanism to access any personal data of any kind through this service. That is to say we don't; no analytics are set up on the website, and no extra code is run on the website to collect any data.")]),W,D,H,e("p",null,[t("The main Pulsar Application, available "),e("a",L,[t("here"),o(a)]),t(" is the program you use anytime you launch Pulsar, or edit text within it.")]),e("p",null,[t("While "),e("a",U,[t("Atom"),o(a)]),t(" the original implementation of the Editor did collect telemetry and metrics about the users, when we began work on the editor one of the first things that we removed was the "),e("a",E,[t("telemetry"),o(a)]),t(".")]),G,R,e("ul",null,[q,V,e("li",null,[t("You can view when this was removed from our upstream "),e("a",O,[t("here"),o(a)]),t(".")])])])}const j=r(d,[["render",N],["__file","index.html.vue"]]);export{j as default}; diff --git a/assets/index.html.39ac8650.js b/assets/index.html.39ac8650.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.39ac8650.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.39d9060f.js b/assets/index.html.39d9060f.js new file mode 100644 index 0000000000..22a7c03661 --- /dev/null +++ b/assets/index.html.39d9060f.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-49ddb9a0","path":"/docs/resources/pulsar-api/","title":"Pulsar API","lang":"en-US","frontmatter":{"title":"Pulsar API"},"excerpt":"","headers":[],"git":{"updatedTime":1694141555000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.13,"words":39},"filePathRelative":"docs/resources/pulsar-api/index.md"}');export{e as data}; diff --git a/assets/index.html.3aa61e11.js b/assets/index.html.3aa61e11.js new file mode 100644 index 0000000000..aa7ee6f116 --- /dev/null +++ b/assets/index.html.3aa61e11.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0da0b37b","path":"/tag/ci/","title":"ci Tag","lang":"en-US","frontmatter":{"title":"ci Tag","blog":{"type":"category","name":"ci","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.3b03ddf1.js b/assets/index.html.3b03ddf1.js new file mode 100644 index 0000000000..5164277d01 --- /dev/null +++ b/assets/index.html.3b03ddf1.js @@ -0,0 +1 @@ +import{_ as i,o as n,c as r,a as e,b as a,d as o,f as s,r as h}from"./app.0e1565ce.js";const l={},c=e("div",{class:"custom-container danger"},[e("p",{class:"custom-container-title"},"STOP"),e("p",null,[a("This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at "),e("a",{href:"/docs/launch-manual/getting-started"},"documentation home"),a(".")])],-1),d={href:"https://flight-manual.atom.io",target:"_blank",rel:"noopener noreferrer"},p=s('This includes their original layout and formatting of how things were expected to appear. Anything listed here, may work, and it may not. This is taking into consideration their sunset and our picking up of their torch.
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
Note
Note: Atom has been at v1.x for over a year, so this appendix is mostly obsolete at this point. We're retaining it for historic and reference purposes.
Atom is at 1.0! Much of the effort leading up to the 1.0 has been cleaning up APIs in an attempt to future proof, and make a more pleasant experience developing packages. If you have developed packages or syntaxes for Atom before the 1.0 API, you can find some tips on upgrading your work in this appendix.
This document will guide you through the large bits of upgrading your package to work with 1.0 APIs.
We've set deprecation messages and errors in strategic places to help make sure you don't miss anything. You should be able to get 95% of the way to an updated package just by fixing errors and deprecations. There are a couple of things you can do to get the full effect of all the errors and deprecations.
If you use any class from require 'atom'
with a $
or View
in the name, add the atom-space-pen-views
module to your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
Then run apm install
in your package directory.
Anywhere you are requiring one of the following from atom
you need to require them from atom-space-pen-views
instead.
# require these from 'atom-space-pen-views' rather than 'atom'
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
So this:
# Old way
+{$, TextEditorView, View, GitRepository} = require 'atom'
+
Would be replaced by this:
# New way
+{GitRepository} = require 'atom'
+{$, TextEditorView, View} = require 'atom-space-pen-views'
+
You wrote specs, right!? Here's where they shine. Run them with cmd-shift-P
, and search for run package specs
. It will show all the deprecation messages and errors.
When you are deprecation free and all done converting, upgrade the engines
field in your package.json:
{
+ "engines": {
+ "atom": ">=0.174.0 <2.0.0"
+ }
+}
+
All of the methods in Atom core that have changes will emit deprecation messages when called. These messages are shown in two places: your package specs, and in Deprecation Cop.
Just run your specs, and all the deprecations will be displayed in yellow.
Note
Note: Deprecations are only displayed when executing specs through the "Window: Run Package Specs" command in the Atom UI. Deprecations are not displayed when running specs at the terminal.
Run Atom in Dev Mode, atom --dev
, with your package loaded, and open Deprecation Cop (search for "deprecation" in the command palette). Deprecated methods will appear in Deprecation Cop only after they have been called.
When Deprecation Cop is open, and deprecated methods are called, a Refresh
button will appear in the top right of the Deprecation Cop interface. So exercise your package, then come back to Deprecation Cop and click the Refresh
button.
Previous to 1.0, views were baked into Atom core. These views were based on jQuery and space-pen
. They looked something like this:
# The old way: getting views from atom
+{$, TextEditorView, View} = require 'atom'
+
+module.exports =
+class SomeView extends View
+ @content: ->
+ @div class: 'find-and-replace', =>
+ @div class: 'block', =>
+ @subview 'myEditor', new TextEditorView(mini: true)
+ #...
+
require 'atom'
no longer provides view helpers or jQuery. Atom Core is now 'view agnostic'. The preexisting view system is available from a new Node module: atom-space-pen-views.
atom-space-pen-views now provides jQuery, space-pen views, and Atom specific views:
# These are now provided by atom-space-pen-views
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
To use the new views, you need to specify the atom-space-pen-views module in your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
space-pen bundles jQuery. If you do not need space-pen or any of the views, you can require jQuery directly.
{
+ "dependencies": {
+ "jquery": "^2"
+ }
+}
+
Sometimes it is as simple as converting the requires at the top of each view page. I assume you read the 'TL;DR' section and have updated all of your requires.
afterAttach
and beforeRemove
updatedThe afterAttach
and beforeRemove
hooks have been replaced with attached
and detached
and the semantics have changed.
afterAttach
was called whenever the node was attached to another DOM node, even if that parent node wasn't present in the DOM. afterAttach
also was called with a boolean indicating whether or not the element and its parents were on the DOM. Now the attached
hook is only called when the node and all of its parents are actually on the DOM, and is not called with a boolean.
beforeRemove
was only called when $.fn.remove
was called, which was typically used when the node was completely removed from the DOM. The new detached
hook is called whenever the DOM node is detached, which could happen if the node is being detached for reattachment later. In short, if beforeRemove
is called the node is never coming back. With detached
it might be attached again later.
# Old way
+{View} = require 'atom'
+class MyView extends View
+ afterAttach: (onDom) ->
+ #...
+
+ beforeRemove: ->
+ #...
+
# New way
+{View} = require 'atom-space-pen-views'
+class MyView extends View
+ attached: ->
+ # Always called with the equivalent of @afterAttach(true)!
+ #...
+
+ detached: ->
+ #...
+
subscribe
and subscribeToCommand
methods removedThe subscribe
and subscribeToCommand
methods have been removed. See the Eventing and Disposables section for more info.
The ScrollView
has very minor changes.
You can no longer use @off
to remove default behavior for core:move-up
, core:move-down
, etc.
# Old way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ super()
+ # turn off default scrolling behavior from ScrollView
+ @off 'core:move-up'
+ @off 'core:move-down'
+ @off 'core:move-left'
+ @off 'core:move-right'
+
# New way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ disposable = super()
+ # turn off default scrolling behavior from ScrollView
+ disposable.dispose()
+
Your SelectListView
might look something like this:
# Old!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ @addClass('command-palette overlay from-top')
+ atom.workspaceView.command 'command-palette:toggle', => @toggle()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ if @hasParent()
+ @cancel()
+ else
+ @attach()
+
+ attach: ->
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ atom.workspaceView.append(this)
+ @focusFilterEditor()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+
This attaches and detaches itself from the DOM when toggled, canceling magically detaches it from the DOM, and it uses the classes overlay
and from-top
.
The new SelectListView no longer automatically detaches itself from the DOM when cancelled. It's up to you to implement whatever cancel behavior you want. Using the new APIs to mimic the semantics of the old class, it should look like this:
# New!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ # no more need for the \`overlay\` and \`from-top\` classes
+ @addClass('command-palette')
+ atom.commands.add 'atom-workspace', 'command-palette:toggle', => @toggle()
+
+ # You need to implement the \`cancelled\` method and hide.
+ cancelled: ->
+ @hide()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ # Toggling now checks panel visibility,
+ # and hides / shows rather than attaching to / detaching from the DOM.
+ if @panel?.isVisible()
+ @cancel()
+ else
+ @show()
+
+ show: ->
+ # Now you will add your select list as a modal panel to the workspace
+ @panel ?= atom.workspace.addModalPanel(item: this)
+ @panel.show()
+
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ @focusFilterEditor()
+
+ hide: ->
+ @panel?.hide()
+
The API no longer exposes any specialized view objects or view classes. atom.workspaceView
, and all the view classes: WorkspaceView
, EditorView
, PaneView
, etc. have been globally deprecated.
Nearly all of the atom-specific actions performed by the old view objects can now be managed via the model layer. For example, here's adding a panel to the interface using the atom.workspace
model instead of the workspaceView
:
# Old!
+div = document.createElement('div')
+atom.workspaceView.appendToTop(div)
+
# New!
+div = document.createElement('div')
+atom.workspace.addTopPanel(item: div)
+
For actions that still require the view, such as dispatching commands or munging css classes, you'll access the view via the atom.views.getView()
method. This will return a subclass of HTMLElement
rather than a jQuery object or an instance of a deprecated view class (e.g. WorkspaceView
).
# Old!
+workspaceView = atom.workspaceView
+editorView = workspaceView.getActiveEditorView()
+paneView = editorView.getPaneView()
+
# New!
+# Generally, just use the models
+workspace = atom.workspace
+editor = workspace.getActiveTextEditor()
+pane = editor.getPane()
+
+# If you need views, get them with \`getView\`
+workspaceElement = atom.views.getView(atom.workspace)
+editorElement = atom.views.getView(editor)
+paneElement = atom.views.getView(pane)
+
atom.workspaceView
, the WorkspaceView
class and the EditorView
class have been deprecated. These two objects are used heavily throughout specs, mostly to dispatch events and commands. This section will explain how to remove them while still retaining the ability to dispatch events and commands.
WorkspaceView
referencesWorkspaceView
has been deprecated. Everything you could do on the view, you can now do on the Workspace
model.
Requiring WorkspaceView
from atom
and accessing any methods on it will throw a deprecation warning. Many specs lean heavily on WorkspaceView
to trigger commands and fetch EditorView
objects.
Your specs might contain something like this:
# Old!
+{WorkspaceView} = require 'atom'
+describe 'FindView', ->
+ beforeEach ->
+ atom.workspaceView = new WorkspaceView()
+
Instead, we will use the atom.views.getView()
method. This will return a plain HTMLElement
, not a WorkspaceView
or jQuery object.
# New!
+describe 'FindView', ->
+ workspaceElement = null
+ beforeEach ->
+ workspaceElement = atom.views.getView(atom.workspace)
+
The workspace needs to be attached to the DOM in some cases. For example, view hooks only work (attached()
on View
, attachedCallback()
on custom elements) when there is a descendant attached to the DOM.
You might see this in your specs:
# Old!
+atom.workspaceView.attachToDom()
+
Change it to:
# New!
+jasmine.attachToDOM(workspaceElement)
+
Like WorkspaceView
, EditorView
has been deprecated. Everything you needed to do on the view you are now able to do on the TextEditor
model.
In many cases, you will not even need to get the editor's view anymore. Any of those instances should be updated to use the TextEditor
instance instead. You should really only need the editor's view when you plan on triggering a command on the view in a spec.
Your specs might contain something like this:
# Old!
+describe 'Something', ->
+ [editorView] = []
+ beforeEach ->
+ editorView = atom.workspaceView.getActiveView()
+
We're going to use atom.views.getView()
again to get the editor element. As in the case of the workspaceElement
, getView
will return a subclass of HTMLElement
rather than an EditorView
or jQuery object.
# New!
+describe 'Something', ->
+ [editor, editorElement] = []
+ beforeEach ->
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
Since the editorElement
objects are no longer jQuery objects, they no longer support trigger()
. Additionally, Atom has a new command dispatcher, atom.commands
, that we use rather than commandeering jQuery's trigger
method.
From this:
# Old!
+workspaceView.trigger 'a-package:toggle'
+editorView.trigger 'find-and-replace:show'
+
To this:
# New!
+atom.commands.dispatch workspaceElement, 'a-package:toggle'
+atom.commands.dispatch editorElement, 'find-and-replace:show'
+
A couple large things changed with respect to events:
`,38),A={href:"https://atom.io/docs/api/latest/Disposable",target:"_blank",rel:"noopener noreferrer"},q=e("code",null,"Disposable",-1),I=e("li",null,[n("The "),e("code",null,"subscribe()"),n(" method is no longer available on "),e("code",null,"space-pen"),n(),e("code",null,"View"),n(" objects")],-1),N=e("li",null,[n("An Emitter is now provided from "),e("code",null,"require 'atom'")],-1),P=e("h5",{id:"consuming-events",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#consuming-events","aria-hidden":"true"},"#"),n(" Consuming Events")],-1),W={href:"https://atom.io/docs/api/latest/Disposable",target:"_blank",rel:"noopener noreferrer"},R=e("code",null,"Disposable",-1),L=e("code",null,"dispose()",-1),U=t(`# Old!
+editor.on 'changed', ->
+
# New!
+disposable = editor.onDidChange ->
+
+# You can unsubscribe at some point in the future via \`dispose()\`
+disposable.dispose()
+
Deprecation warnings will guide you toward the correct methods.
CompositeDisposable
You can group multiple disposables into a single disposable with a CompositeDisposable
.
{CompositeDisposable} = require 'atom'
+
+class Something
+ constructor: ->
+ editor = atom.workspace.getActiveTextEditor()
+ @disposables = new CompositeDisposable
+ @disposables.add editor.onDidChange ->
+ @disposables.add editor.onDidChangePath ->
+
+ destroy: ->
+ @disposables.dispose()
+
View::subscribe
and Subscriber::subscribe
callsThere were a couple permutations of subscribe()
. In these examples, a CompositeDisposable
is used as it will commonly be useful where conversion is necessary.
subscribe(unsubscribable)
This one is very straight forward.
# Old!
+@subscribe editor.on 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(modelObject, event, method)
When the modelObject is an Atom model object, the change is very simple. Just use the correct event method, and add it to your CompositeDisposable.
# Old!
+@subscribe editor, 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(jQueryObject, selector(optional), event, method)
Things are a little more complicated when subscribing to a DOM or jQuery element. Atom no longer provides helpers for subscribing to elements. You can use jQuery or the native DOM APIs, whichever you prefer.
# Old!
+@subscribe $(window), 'focus', ->
+
# New!
+{Disposable, CompositeDisposable} = require 'atom'
+disposables = new CompositeDisposable
+
+# New with jQuery
+focusCallback = ->
+$(window).on 'focus', focusCallback
+disposables.add new Disposable ->
+ $(window).off 'focus', focusCallback
+
+# New with native APIs
+focusCallback = ->
+window.addEventListener 'focus', focusCallback
+disposables.add new Disposable ->
+ window.removeEventListener 'focus', focusCallback
+
Emitter
# New!
+{Emitter} = require 'atom'
+
+class Something
+ constructor: ->
+ @emitter = new Emitter
+
+ destroy: ->
+ @emitter.dispose()
+
+ onDidChange: (callback) ->
+ @emitter.on 'did-change', callback
+
+ methodThatFiresAChange: ->
+ @emitter.emit 'did-change', {data: 2}
+
+# Using the evented class
+something = new Something
+something.onDidChange (eventObject) ->
+ console.log eventObject.data # => 2
+something.methodThatFiresAChange()
+
# Old!
+atom.workspaceView.command 'core:close core:cancel', ->
+
+# When inside a View class, you might see this
+@subscribeToCommand 'core:close core:cancel', ->
+
# New!
+@disposables.add atom.commands.add 'atom-workspace',
+ 'core:close': ->
+ 'core:cancel': ->
+
+# You can register commands directly on individual DOM elements in addition to
+# using selectors. When in a View class, you should have a \`@element\` object
+# available. \`@element\` is a plain HTMLElement object
+@disposables.add atom.commands.add @element,
+ 'core:close': ->
+ 'core:cancel': ->
+
Note
Note: The Shadow DOM was removed in Atom 1.13
. The ::shadow
and /deep/
selectors and the context-targeted style sheets described below won't work and should not be used anymore.
In addition to changes in Atom's scripting API, we'll also be making some breaking changes to Atom's DOM structure, requiring style sheets and keymaps in both packages and themes to be updated.
Deprecation Cop will list usages of deprecated selector patterns to guide you. You can access it via the Command Palette (cmd-shift-p
, then search for Deprecation
). It breaks the deprecations down by package:
Rather than adding classes to standard HTML elements to indicate their role, Atom now uses custom element names. For example, <div class="workspace">
has now been replaced with <atom-workspace>
. Selectors should be updated accordingly. Note that tag names have lower specificity than classes in CSS, so you'll need to take care in converting things.
Old Selector | New Selector |
---|---|
.editor | atom-text-editor |
.editor.mini | atom-text-editor[mini] |
.workspace | atom-workspace |
.horizontal | atom-workspace-axis.horizontal |
.vertical | atom-workspace-axis.vertical |
.pane-container | atom-pane-container |
.pane | atom-pane |
.tool-panel | atom-panel |
.panel-top | atom-panel.top |
.panel-bottom | atom-panel.bottom |
.panel-left | atom-panel.left |
.panel-right | atom-panel.right |
.overlay | atom-panel.modal |
::shadow
The ::shadow
pseudo-element allows you to bypass a single shadow root. For example, say you want to update a highlight decoration for a linter package. Initially, the style looks as follows:
// Without shadow DOM support
+atom-text-editor .highlight.my-linter {
+ background: hotpink;
+}
+
In order for this style to apply with the shadow DOM enabled, you will need to add a second selector with the ::shadow
pseudo-element. You should leave the original selector in place so your theme continues to work with the shadow DOM disabled during the transition period.
// With shadow DOM support
+atom-text-editor .highlight.my-linter,
+atom-text-editor::shadow .highlight.my-linter {
+ background: hotpink;
+}
+
/deep/
The /deep/
combinator overrides all shadow boundaries, making it useful for rules you want to apply globally such as scrollbar styling. Here's a snippet containing scrollbar styling for the Atom Dark UI theme before shadow DOM support:
// Without shadow DOM support
+.scrollbars-visible-always {
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track,
+ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
To style scrollbars even inside of the shadow DOM, each rule needs to be prefixed with /deep/
. We use /deep/
instead of ::shadow
because we don't care about the selector of the host element in this case. We just want our styling to apply everywhere.
// With shadow DOM support using /deep/
+.scrollbars-visible-always {
+ /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ /deep/ ::-webkit-scrollbar-track,
+ /deep/ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ /deep/ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
The selector features discussed above allow you to target shadow DOM content with specific selectors, but Atom also allows you to target a specific shadow DOM context with an entire style sheet. The context into which a style sheet is loaded is based on the file name. If you want to load a style sheet into the editor, name it with the .atom-text-editor.less
or .atom-text-editor.css
extensions.
my-ui-theme/
+ styles/
+ index.less # loaded globally
+ index.atom-text-editor.less # loaded in the text editor shadow DOM
+
Inside a context-targeted style sheet, there's no need to use the ::shadow
or /deep/
expressions. If you want to refer to the element containing the shadow root, you can use the ::host
pseudo-element.
During the transition phase, style sheets targeting the atom-text-editor
context will also be loaded globally. Make sure you update your selectors in a way that maintains compatibility with the shadow DOM being disabled. That means if you use a ::host
pseudo element, you should also include the same style rule matches against atom-text-editor
.
Note
Note: The Shadow DOM was removed in Atom 1.13
. The :host
selector described below won't work and should not be used anymore.
Here's an example from Atom's light syntax theme. Note that the atom-text-editor
selector intended to target the editor from the outside has been retained to allow the theme to keep working during the transition phase when it is possible to disable the shadow DOM.
atom-text-editor,
+:host {
+ /* :host added */
+ background-color: @syntax-background-color;
+ color: @syntax-text-color;
+
+ .invisible-character {
+ color: @syntax-invisible-character-color;
+ }
+ /* more nested selectors... */
+}
+
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, you use this at your own risk. Current Pulsar documentation for this section is found at the documentation home.
In this chapter, we're going to learn how to extend the functionality of Atom through writing packages. This will be everything from new user interfaces to new language grammars to new themes. We'll learn this by writing a series of increasingly complex packages together, introducing you to new APIs and tools and techniques as we need them.
If you're looking for an example using a specific API or feature, you can skip to the end of the chapter where we've indexed all the examples that way.
To begin, there are a few things we'll assume you know, at least to some degree. Since all of Atom is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.
While much of Atom has been converted to JavaScript, a lot of older code has been left implemented in CoffeeScript because changing it would have been too risky. Additionally, Atom's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:
MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
You can open the init.coffee
file in an editor from the Atom > Init ScriptFile > Init ScriptEdit > Init Script menu. This file can also be named init.js
and contain JavaScript code.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.coffee
file to have Atom greet you with an audio beep every time it loads:
atom.beep()
+
atom.commands.add 'atom-text-editor', 'markdown:paste-as-link', ->
+ return unless editor = atom.workspace.getActiveTextEditor()
+
+ selection = editor.getLastSelection()
+ clipboardText = atom.clipboard.read()
+
+ selection.insertText("[#{selection.getText()}](#{clipboardText})")
+
You can see that Atom has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+\u251C\u2500 grammars/
+\u251C\u2500 keymaps/
+\u251C\u2500 lib/
+\u251C\u2500 menus/
+\u251C\u2500 spec/
+\u251C\u2500 snippets/
+\u251C\u2500 styles/
+\u251C\u2500 index.js
+\u2514\u2500 package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Atom will default to looking for an index.coffee
or index.js
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Atom has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Warning: Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Atom's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Atom. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
`,11),ye=e("li",null,[e("code",null,"activate(state)"),n(": This "),e("strong",null,"optional"),n(" method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the "),e("code",null,"serialize()"),n(" method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).")],-1),we=e("code",null,"initialize(state)",-1),_e=e("strong",null,"optional",-1),xe=e("code",null,"activate()",-1),qe=e("code",null,"initialize()",-1),Ae=e("code",null,"activate()",-1),Te=e("code",null,"initialize()",-1),Ce=e("li",null,[e("code",null,"serialize()"),n(": This "),e("strong",null,"optional"),n(" method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's "),e("code",null,"activate"),n(" method so you can restore your view to where the user left off.")],-1),Se=e("li",null,[e("code",null,"deactivate()"),n(": This "),e("strong",null,"optional"),n(" method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.")],-1),Ie=e("h5",{id:"style-sheets",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#style-sheets","aria-hidden":"true"},"#"),n(" Style Sheets")],-1),je=e("code",null,"styles",-1),Pe={href:"http://lesscss.org",target:"_blank",rel:"noopener noreferrer"},Re=e("p",null,[n("Ideally, you won't need much in the way of styling. Atom provides a standard set of components which define both the colors and UI elements for any package that fits into Atom seamlessly. You can view all of Atom's UI components by opening the styleguide: open the command palette "),e("kbd",{class:"platform-mac"},"Cmd+Shift+P"),e("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+P"),n(" and search for "),e("code",null,"styleguide"),n(", or type "),e("kbd",{class:"platform-mac"},"Cmd+Ctrl+Shift+G"),e("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+G"),n(".")],-1),Ne=e("em",null,"do",-1),Ee=e("em",null,"must",-1),Ue={href:"https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less",target:"_blank",rel:"noopener noreferrer"},Me=t(`An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Atom UI, this means the key combination will work anywhere in the application.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your plugin.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Atom text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole plugin.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Atom but optional. The toggle
method is one Atom is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Atom starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Atom workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple plugin. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the plugin so other plugins could subscribe to these events.
// Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Tip: Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = \`There are \${count} words.\`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
Note: To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Atom windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Atom being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press Alt+Cmd+ICtrl+Shift+I, or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Atom.
Once you've got your test suite written, you can run it by pressing Alt+Cmd+Ctrl+PAlt+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the atom --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
o888
+ ooooooo ooooooo ooooooo 888
+ 888 888 888 888 888 888 888
+ 888 888 888 888 888 888
+ 88ooo888 88ooo88 88ooo88 o888o
+
+
This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.
The final package can be viewed at https://github.com/atom/ascii-art.
Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing Alt+Cmd+Ctrl+LCtrl+Shift+F5.
Now open the Command Palette and search for the "Ascii Art: Convert" command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Atom launch faster by allowing Atom to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the "Ascii Art: Convert" command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
Warning: The Atom keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command "Update Package Dependencies: Update" from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run "Update Package Dependencies: Update" whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(\`\\n\${art}\\n\`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
`,6),rn={href:"https://atom.io/docs/api/latest/TextEditor#instance-getSelectedText",target:"_blank",rel:"noopener noreferrer"},pn=e("code",null,"editor.getSelectedText()",-1),dn={href:"https://atom.io/docs/api/latest/TextEditor#instance-insertText",target:"_blank",rel:"noopener noreferrer"},un=e("code",null,"editor.insertText()",-1),hn=e("h4",{id:"summary-1",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#summary-1","aria-hidden":"true"},"#"),n(" Summary")],-1),mn=e("p",null,"In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.",-1),gn=e("h3",{id:"package-active-editor-info",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#package-active-editor-info","aria-hidden":"true"},"#"),n(" Package: Active Editor Info")],-1),kn={href:"https://nuclide.io",target:"_blank",rel:"noopener noreferrer"},vn=e("p",null,"For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at https://github.com/atom/active-editor-info.",-1),bn=e("h4",{id:"create-the-package",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#create-the-package","aria-hidden":"true"},"#"),n(" Create the Package")],-1),fn=e("kbd",{class:"platform-mac"},"Cmd+Shift+P",-1),yn=e("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+P",-1),wn={href:"https://github.com/atom/command-palette",target:"_blank",rel:"noopener noreferrer"},_n=e("code",null,"active-editor-info",-1),xn=t(`Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Atom. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Atom will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Atom uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Atom for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Atom to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the "Active Editor Info: Toggle" command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = \`
+ <h2>\${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> \${item.softWrapped}</li>
+ <li><b>Tab Length:</b> \${item.getTabLength()}</li>
+ <li><b>Encoding:</b> \${item.getEncoding()}</li>
+ <li><b>Line Count:</b> \${item.getLineCount()}</li>
+ </ul>
+ \`;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Atom how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Atom and toggle the view with the "Active Editor Info: Toggle" command. Then reload Atom again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
Atom supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the Atom > PreferencesFile > PreferencesEdit > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
',5),Tn={href:"https://speakerdeck.com/danmatthews/less-css",target:"_blank",rel:"noopener noreferrer"},Cn=e("code",null,"package.json",-1),Sn=e("code",null,"package.json",-1),In=e("li",null,[n("Your theme's "),e("code",null,"package.json"),n(" must contain a "),e("code",null,"theme"),n(" key with a value of "),e("code",null,"ui"),n(" or "),e("code",null,"syntax"),n(" for Atom to recognize and load it as a theme.")],-1),jn={href:"https://atom.io/themes",target:"_blank",rel:"noopener noreferrer"},Pn=t('Let's create your first theme.
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Syntax Theme" to generate a new theme package. Select "Generate Syntax Theme," and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Tip: Syntax themes should end with -syntax and UI themes should end with -ui.
Atom will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with Cmd+,Ctrl+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Atom by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R to see the changes you made reflected in your Atom window. Pretty neat!
Tip
Tip: You can avoid reloading to see changes you make by opening an Atom window in Dev Mode. To open a Dev Mode Atom window run atom --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
Note: It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Atom's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
',13),Rn={href:"https://github.com/atom-community/ui-theme-template",target:"_blank",rel:"noopener noreferrer"},Nn=t('atom --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
apm link --dev
to symlink your repository to ~/.atom/dev/packages
Tip
Tip: Because we used apm link --dev
in the above instructions, if you break anything you can always close Atom and launch Atom normally to force Atom to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
',4),Un={href:"https://github.com/atom/atom/blob/master/static/variables/ui-variables.less",target:"_blank",rel:"noopener noreferrer"},Mn={href:"https://github.com/atom/atom/blob/master/static/variables/syntax-variables.less",target:"_blank",rel:"noopener noreferrer"},Wn=e("p",null,"These default values will be used as a fallback in case a theme doesn't define its own variables.",-1),Fn=e("h5",{id:"use-in-packages",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#use-in-packages","aria-hidden":"true"},"#"),n(" Use in Packages")],-1),Dn=e("p",null,[n("In any of your package's "),e("code",null,".less"),n(" files, you can access the theme variables by importing the "),e("code",null,"ui-variables"),n(" or "),e("code",null,"syntax-variables"),n(" file from Atom.")],-1),Ln={href:"https://github.com/atom/styleguide",target:"_blank",rel:"noopener noreferrer"},On=t(`Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
To launch a Dev Mode window:
atom --dev
If you'd like to reload all the styles at any time, you can use the shortcut Alt+Cmd+Ctrl+LAlt+Ctrl+R.
Atom is based on the Chrome browser, and supports Chrome's Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the Alt+Cmd+ICtrl+Shift+I shortcut.
The dev tools allow you to inspect elements and take a look at their CSS properties.
To open the Styleguide, open the command palette with Cmd+Shift+PCtrl+Shift+P and search for "styleguide", or use the shortcut Cmd+Ctrl+Shift+GCtrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes un-usable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Atom in "normal" mode to fix the issue, it's advised to open two Atom windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Atom users, it's time to publish it. \u{1F389}
',9),Kn=e("h3",{id:"creating-a-grammar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#creating-a-grammar","aria-hidden":"true"},"#"),n(" Creating a Grammar")],-1),Xn={href:"http://tree-sitter.github.io/tree-sitter",target:"_blank",rel:"noopener noreferrer"},Zn={href:"https://en.wikipedia.org/wiki/Abstract_syntax_tree",target:"_blank",rel:"noopener noreferrer"},Qn=e("em",null,"syntax trees",-1),ea=t('This syntax tree gives Atom a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Atom are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Atom: a parser and a grammar file.
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Atom package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+\u251C\u2500\u2500 LICENSE
+\u251C\u2500\u2500 README.md
+\u251C\u2500\u2500 grammars
+\u2502 \u2514\u2500\u2500 mylanguage.cson
+\u2514\u2500\u2500 package.json
+
The mylanguage.cson
file specifies how Atom should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
Next, the file should contain some fields that indicate to Atom when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Atom uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
scopes:\n '''\n "*",\n "/",\n "+",\n "-"\n ''': 'keyword.operator'\n
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:\n\n # Apply the classes `syntax--builtin` and `syntax--variable` to all\n # `identifier` nodes whose text is `require`.\n 'identifier': {exact: 'require', scopes: 'builtin.variable'},\n\n # Apply the classes `syntax--type` and `syntax--integer` to all\n # `primitive_type` nodes whose text starts with `int` or `uint`.\n 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},\n\n # Apply the classes `syntax--builtin`, `syntax--class`, and\n # `syntax--name` to `constant` nodes with the text `Array`,\n # `Hash` and `String`. For all other `constant` nodes, just\n # apply the classes `syntax--class` and `syntax--name`.\n 'constant': [\n {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},\n 'class.name'\n ]\n
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector \`call_expression > identifier\` is more
+ # specific than the selector \`identifier\`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
// HTML in a template literal
+const htmlContent = html\`<div>Hello \${name}</div>\`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All \`comment\` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # \`if_statement\` nodes are foldable if they contain an anonymous
+ # "then" token and either an \`elif_clause\` or \`else_clause\` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # \`elif_clause\` or \`else_clause\`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Atom's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or un-comment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Atom:
`,23),za={href:"https://github.com/atom/language-shellscript",target:"_blank",rel:"noopener noreferrer"},Ya={href:"https://github.com/atom/language-c",target:"_blank",rel:"noopener noreferrer"},Va={href:"https://github.com/atom/language-go",target:"_blank",rel:"noopener noreferrer"},$a={href:"https://github.com/atom/language-html",target:"_blank",rel:"noopener noreferrer"},Ga={href:"https://github.com/atom/language-javascript",target:"_blank",rel:"noopener noreferrer"},Ha={href:"https://github.com/atom/language-python",target:"_blank",rel:"noopener noreferrer"},Ba={href:"https://github.com/atom/language-ruby",target:"_blank",rel:"noopener noreferrer"},Ja={href:"https://github.com/atom/language-typescript",target:"_blank",rel:"noopener noreferrer"},Ka=t('Atom's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Atom's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
Note
Note: This tutorial is a work in progress.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Atom uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Tip: Grammar packages should start with language-.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \\bFlight Manual\\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\\\bFlight Manual\\\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
Tip
Tip: All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Note: Astute readers may have noticed that the \\b
was changed to \\\\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\\\b(Flight) (Manual)\\\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the ::: note Note
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. ::: note Note
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Tip: Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\\\b(Flight) (Manual)\\\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
`,19),ms={href:"https://gist.github.com/DamnedScholar/622926bcd222eb1ddc483d12103fd315",target:"_blank",rel:"noopener noreferrer"},gs={href:"https://gist.github.com/Aerijo/b8c82d647db783187804e86fa0a604a1",target:"_blank",rel:"noopener noreferrer"},ks=e("li",null,"http://www.apeth.com/nonblog/stories/textmatebundle.html. A blog of a programmer's experience writing a grammar package for TextMate.",-1),vs={href:"https://github.com/kkos/oniguruma/blob/master/doc/RE",target:"_blank",rel:"noopener noreferrer"},bs={href:"http://manual.macromates.com/en/language_grammars.html",target:"_blank",rel:"noopener noreferrer"},fs={href:"https://github.com/atom/first-mate",target:"_blank",rel:"noopener noreferrer"},ys=e("code",null,"first-mate",-1),ws={href:"https://github.com/atom/language-python",target:"_blank",rel:"noopener noreferrer"},_s={href:"https://github.com/atom/language-javascript",target:"_blank",rel:"noopener noreferrer"},xs={href:"https://github.com/atom/language-html",target:"_blank",rel:"noopener noreferrer"},qs={href:"https://github.com/atom?utf8=%E2%9C%93&q=language&type=source&language=",target:"_blank",rel:"noopener noreferrer"},As=e("h3",{id:"publishing",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#publishing","aria-hidden":"true"},"#"),n(" Publishing")],-1),Ts=e("code",null,"apm",-1),Cs=e("code",null,"apm",-1),Ss=e("h4",{id:"prepare-your-package",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#prepare-your-package","aria-hidden":"true"},"#"),n(" Prepare Your Package")],-1),Is=e("p",null,"There are a few things you should double check before publishing:",-1),js=t("package.json
file has name
, description
, and repository
fields.package.json
file has a version
field with a value of "0.0.0"
.package.json
file has an engines
field that contains an entry for Atom such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ apm publish minor
+
Your package is now published and available on atom.io. Head on over to https://atom.io/packages/your-package-name
to see your package's page.
With apm publish
, you can bump the version and publish by using
$ apm publish <em>version-type</em>
+
where version-type
can be major
, minor
and patch
.
The major
option to the publish command tells apm to increment the first number of the version before publishing so the published version will be 1.0.0
and the Git tag created will be v1.0.0
.
The minor
option to the publish command tells apm to increment the second number of the version before publishing so the published version will be 0.1.0
and the Git tag created will be v0.1.0
.
The patch
option to the publish command tells apm to increment the third number of the version before publishing so the published version will be 0.0.1
and the Git tag created will be v0.0.1
.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
You might be running into an issue which was already fixed in a more recent version of Atom than the one you're using.
If you're using a released version, check which version of Atom you're using:
$ atom --version
+> Atom : 1.8.0
+> Electron: 0.36.8
+> Chrome : 47.0.2526.110
+> Node : 5.1.1
+
A large part of Atom's functionality comes from packages you can install. Atom will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Atom from the terminal in safe mode:
$ atom --safe
+
This starts Atom, but does not load packages from ~/.atom/packages
or ~/.atom/dev/packages
and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Atom normally still produces the error, then try figuring out which package is causing trouble. Start Atom normally again and open the Settings View with Cmd+,Ctrl+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Atom or reload Atom with Alt+Cmd+Ctrl+LCtrl+Shift+F5 after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Atom saves a number of things about your environment when you exit in order to restore Atom to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Atom from working properly. In these cases, you may want to clear the state that Atom has saved.
DANGER
:rotatinglight: Danger: Clearing the saved state permanently destroys any state that Atom has saved _across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ atom --clear-window-state
+
In some cases, you may want to reset Atom to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
`,14),vt=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ mv ~/.atom ~/.atom-backup +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),bt=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ mv ~/.atom ~/.atom-backup +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),ft=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ rename %USERPROFILE%\\.atom .atom-backup +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),yt=t(`Once that is complete, you can launch Atom as normal. Everything will be just as if you first installed Atom.
Tip
Tip: The command given above doesn't delete the old configuration, just puts it somewhere that Atom can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the ~/.atom-backup
%USERPROFILE%\\.atom-backup
directory.
If you develop or contribute to Atom packages, there may be left-over packages linked to your ~/.atom/packages
or ~/.atom/dev/packages
directories. You can use the apm links
command to list all linked packages:
$ apm links
+> /Users/octocat/.atom/dev/packages (0)
+> \u2514\u2500\u2500 (no links)
+> /Users/octocat/.atom/packages (1)
+> \u2514\u2500\u2500 color-picker -> /Users/octocat/github/color-picker
+
You can remove links using the apm unlink
command:
$ apm unlink color-picker
+> Unlinking /Users/octocat/.atom/packages/color-picker \u2713
+
See apm links --help
and apm unlink --help
for more information on these commands.
Tip
Tip: You can also use apm unlink --all
to easily unlink all packages and themes.
Show the keybinding resolver with Cmd+.Ctrl+. or with "Keybinding Resolver: Show" from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
',5),zt=e("ul",null,[e("li",null,[e("p",null,"The key combination was not used in the context defined by the keybinding's selector"),e("p",null,[n("For example, you can't trigger the keybinding for the "),e("code",null,"tree-view:add-file"),n(" command if the Tree View is not focused.")])]),e("li",null,[e("p",null,"There is another keybinding that took precedence"),e("p",null,"This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.")])],-1),Yt=e("code",null,"keymap.cson",-1),Vt={href:"https://github.com/atom/atom/discussions",target:"_blank",rel:"noopener noreferrer"},$t=e("h4",{id:"check-font-rendering-issues",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#check-font-rendering-issues","aria-hidden":"true"},"#"),n(" Check Font Rendering Issues")],-1),Gt=e("kbd",{class:"platform-mac"},"Alt+Cmd+I",-1),Ht=e("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+I",-1),Bt={href:"https://developers.google.com/web/tools/chrome-devtools/inspect-styles/",target:"_blank",rel:"noopener noreferrer"},Jt=t('When an unexpected error occurs in Atom, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press Alt-Cmd-ICtrl-Shift-I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
Note: When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
To run a profile, open the Developer Tools with Alt+Cmd+ICtrl+Shift+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Atom:
$ atom --profile-startup .
+
This will automatically capture a CPU profile as Atom is loading and open the Developer Tools once Atom loads. From there:
You can then include the startup profile in any Issue you report.
If you are having issues installing a package using apm install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run apm install --check
to see if the Atom package manager can build native code on your machine.
If you encounter flickering or other rendering issues, you can stop Atom from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ atom --disable-gpu
+
Chromium (and thus Atom) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Atom to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ atom --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Atom flags as they will not be executed after the Chromium flags e.g.:
$ atom --safe --enable-gpu-rasterization --force-gpu-rasterization
+
We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Atom?
`,11),go={href:"https://jasmine.github.io/archives/1.3/introduction",target:"_blank",rel:"noopener noreferrer"},ko=e("h4",{id:"create-a-new-spec",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#create-a-new-spec","aria-hidden":"true"},"#"),n(" Create a New Spec")],-1),vo={href:"https://github.com/atom/atom/tree/master/spec",target:"_blank",rel:"noopener noreferrer"},bo={href:"https://github.com/atom/markdown-preview/tree/master/spec",target:"_blank",rel:"noopener noreferrer"},fo=e("code",null,"spec",-1),yo=t(`Spec files must end with -spec
so add sample-spec.coffee
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Atom includes the following:
`,3),xo={href:"https://github.com/velesin/jasmine-jquery",target:"_blank",rel:"noopener noreferrer"},qo=t("toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domWriting Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Atom. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Atom core specs when working on Atom itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
To run tests on the command line, run Atom with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
atom --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
WARNING
Warning: This API is available as of 1.2.0-beta0, and it is experimental and subject to change. Test runner authors should be prepared to test their code against future beta releases until it stabilizes.
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Atom will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Atom will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Atom's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.atom
).enablePersistence
A boolean indicating whether the Atom environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via atom --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finish running. This exit code will be returned when running your tests via the command line.
Beginning in Atom 1.23, packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Warning: Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Atom package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Atom which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Atom from deferring activation of your package \u2014\xA0see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Atom handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings \u2014\xA0you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Atom will not activate your package until it has a URI for it to handle \u2014\xA0it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately \u2014\xA0disabling deferred activation means Atom takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Atom on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger atom with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Atom provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Atom runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)"c:\\my test"
/my\\ test
\\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\\
and d:\\
CRLF
LF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
You can convert the R bundle with the following command:
$ apm init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the apm link
command, your new package is ready to use. Launch Atom and open a .r
file in the editor to see it in action!
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ apm init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Atom and opening the Settings View with the Atom > PreferencesFile > PreferencesEdit > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
If you're hitting a bug in Atom or just want to experiment with adding a feature to the core of the system, you'll want to run Atom in Dev Mode with access to a local copy of the Atom source.
Once you've set up your fork of the atom/atom repository, you can clone it to your local machine:
$ git clone git@github.com:<em>your-username</em>/atom.git
+
From there, you can navigate into the directory where you've cloned the Atom source code and run the bootstrap script to install all the required dependencies:
`,4),ai=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ cd where-you-cloned-atom +$ script/bootstrap +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),si=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ cd where-you-cloned-atom +$ script/bootstrap +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),ti=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ cd where-you-cloned-atom +$ script\\bootstrap +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),oi=t(`Once you have a local copy of Atom cloned and bootstrapped, you can then run Atom in Development Mode. But first, if you cloned Atom to somewhere other than ~/github/atom
%USERPROFILE%\\github\\atom
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Atom. To run Atom in Dev Mode, use the --dev
parameter from the terminal:
$ atom --dev <em>path-to-open</em>
+
Note
Note: If the atom command does not respond in the terminal, then try atom-dev or atom-beta. The suffix depends upon the particular source code that was cloned.
There are a couple benefits of running Atom in Dev Mode:
`,5),ii=t('ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Atom is run using the source code from your local atom/atom
repository. This means that you don't have to run script/build
script\\build
every time you change code. Just restart Atom \u{1F44D}~/.atom/dev/packages
%USERPROFILE%\\.atom\\dev\\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.In order to run Atom Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <em>path-to-your-local-atom-repo</em>
+$ atom --test spec
+
In order to build Atom from source, you need to have a number of other requirements and take additional steps.
`,5),pi=e("p",null,"Ubuntu LTS 16.04 64-bit is the recommended platform.",-1),di=e("h5",{id:"requirements",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#requirements","aria-hidden":"true"},"#"),n(" Requirements")],-1),ui=e("li",null,"OS with 64-bit or 32-bit architecture",-1),hi=e("li",null,"C++11 toolchain",-1),mi=e("li",null,"Git",-1),gi={href:"https://github.com/creationix/nvm",target:"_blank",rel:"noopener noreferrer"},ki=e("li",null,[n("npm 6.12 or later (run "),e("code",null,"npm install -g npm"),n(")")],-1),vi=e("li",null,"Python 2.6.x, 2.7.x or 3.5+",-1),bi={href:"https://wiki.gnome.org/Projects/Libsecret",target:"_blank",rel:"noopener noreferrer"},fi=e("p",null,"For more details, scroll down to find how to setup a specific Linux distro.",-1),yi=e("h6",{id:"ubuntu-debian",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#ubuntu-debian","aria-hidden":"true"},"#"),n(" Ubuntu / Debian")],-1),wi=e("ul",null,[e("li",null,[e("p",null,"Install GNOME headers and other basic prerequisites:"),e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo apt-get install build-essential git libsecret-1-dev fakeroot rpm libx11-dev libxkbfile-dev +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])])]),e("li",null,[e("p",null,[n("If "),e("code",null,"script/build"),n(" exits with an error, you may need to install a newer C++ compiler with C++11:")]),e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo add-apt-repository ppa:ubuntu-toolchain-r/test +$ sudo apt-get update +$ sudo apt-get install gcc-5 g++-5 +$ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-5 80 --slave /usr/bin/g++ g++ /usr/bin/g++-5 +$ sudo update-alternatives --config gcc # choose gcc-5 from the list +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])])])],-1),_i=e("h6",{id:"fedora-22",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#fedora-22","aria-hidden":"true"},"#"),n(" Fedora 22+")],-1),xi=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo dnf --assumeyes install make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools libX11-devel libxkbfile-devel +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),qi=e("h6",{id:"fedora-21-centos-rhel",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#fedora-21-centos-rhel","aria-hidden":"true"},"#"),n(" Fedora 21 / CentOS / RHEL")],-1),Ai=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo yum install -y make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Ti=e("h6",{id:"arch",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#arch","aria-hidden":"true"},"#"),n(" Arch")],-1),Ci=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`sudo pacman -S --needed gconf base-devel git nodejs npm libsecret python libx11 libxkbfile +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Si=e("h6",{id:"slackware",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#slackware","aria-hidden":"true"},"#"),n(" Slackware")],-1),Ii=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sbopkg -k -i node -i atom +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),ji=e("h6",{id:"opensuse",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#opensuse","aria-hidden":"true"},"#"),n(" openSUSE")],-1),Pi=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo zypper install nodejs nodejs-devel make gcc gcc-c++ glibc-devel git-core libsecret-devel rpmdevtools libX11-devel libxkbfile-devel +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Ri=e("h5",{id:"requirements-1",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#requirements-1","aria-hidden":"true"},"#"),n(" Requirements")],-1),Ni=e("li",null,"macOS 10.9 or later",-1),Ei={href:"https://github.com/creationix/nvm",target:"_blank",rel:"noopener noreferrer"},Ui=e("li",null,[n("npm 6.12 or later (run "),e("code",null,"npm install -g npm"),n(")")],-1),Mi=e("li",null,"Python v2.6.x, v2.7.x or v3.5+",-1),Wi={href:"https://developer.apple.com/xcode/downloads/",target:"_blank",rel:"noopener noreferrer"},Fi=e("code",null,"xcode-select --install",-1),Di=e("li",null,[e("p",null,"Node.js 10.12 or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom)")],-1),Li=e("li",null,[e("p",null,[n("npm 6.12 or later (run "),e("code",null,"npm install -g npm"),n(")")])],-1),Oi=e("p",null,"Python v2.6.x, v2.7.x, or v3.5+",-1),zi={href:"https://www.microsoft.com/en-us/search/shop/apps?q=python+software+foundation&devicetype=pc&category=Developer+tools%5cDevelopment+kits&Price=0&MaturityRating=ESRB%3aE",target:"_blank",rel:"noopener noreferrer"},Yi=e("li",null,[n("Download Python from https://www.python.org/downloads/. "),e("ul",null,[e("li",null,'For Python 2, be sure to install in the default location, or check "Add Python 2.x to PATH" before installing.'),e("li",null,[n('For Python 3, check "Add Python 3.x to PATH", or change the install path to '),e("code",null,"[Your_Drive_Letter]:\\Python37"),n(" e.g. "),e("code",null,"C:\\Python37"),n(", (even if your version of Python 3 isn't 3.7, that's one place where the scripts will look.)")]),e("li",null,[n("If python isn't found by the bootstrap script, create a symbolic link to the directory containing "),e("code",null,"python.exe"),n(" using e.g.: "),e("code",null,"mklink /d %SystemDrive%\\Python27 D:\\elsewhere\\Python27"),n("(Links should be set at either "),e("code",null,"%SystemDrive%\\Python27"),n(" or "),e("code",null,"%SystemDrive%\\Python37"),n(", regardless of what version of Python you actually have.)")])])],-1),Vi=e("p",null,"C++ build tools:",-1),$i=e("strong",null,"Option 1:",-1),Gi={href:"https://www.npmjs.com/package/windows-build-tools",target:"_blank",rel:"noopener noreferrer"},Hi=e("code",null,"npm install --global windows-build-tools@4",-1),Bi=e("strong",null,"Option 2:",-1),Ji={href:"https://visualstudio.microsoft.com/visual-cpp-build-tools/",target:"_blank",rel:"noopener noreferrer"},Ki=e("strong",null,"Option 3:",-1),Xi={href:"https://www.visualstudio.com/downloads/",target:"_blank",rel:"noopener noreferrer"},Zi=e("p",null,"Also ensure that:",-1),Qi=e("ul",null,[e("li",null,"The default installation folder is chosen so the build tools can find it"),e("li",null,"If using Visual Studio make sure Visual C++ support is selected/installed"),e("li",null,"If using Visual C++ Build Tools make sure a Windows SDK (Windows 8 SDK or Windows 10 SDK) is selected/installed"),e("li",null,[n("A "),e("code",null,"git"),n(" command is in your path")]),e("li",null,[n("Set the "),e("code",null,"GYP_MSVS_VERSION"),n(" environment variable to the Visual Studio/Build Tools version ("),e("code",null,"2015"),n(" or "),e("code",null,"2017"),n(".) e.g. "),e("code",null,'[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")'),n(" in PowerShell (or set it in Windows advanced system settings).")])],-1),el=e("h5",{id:"instructions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#instructions","aria-hidden":"true"},"#"),n(" Instructions")],-1),nl=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ script/build +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),al=e("p",null,[n("To also install the newly built application, use the "),e("code",null,"--create-debian-package"),n(" or "),e("code",null,"--create-rpm-package"),n(" option and then install the generated package via the system package manager.")],-1),sl=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ script/build +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),tl=e("p",null,[n("To also install the newly built application, use "),e("code",null,"script/build --install"),n(".")],-1),ol=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ script\\build +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),il=e("p",null,[n("To also install the newly built application, use "),e("code",null,"script\\build --create-windows-installer"),n(" and launch one of the generated installers.")],-1),ll=e("h5",{id:"script-build-options",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#script-build-options","aria-hidden":"true"},"#"),n(),e("code",null,"script/build"),n(" Options")],-1),cl=e("ul",null,[e("li",null,[e("code",null,"--compress-artifacts"),n(": zips the generated application as "),e("code",null,"out/atom-{arch}.tar.gz"),n(".")]),e("li",null,[e("code",null,"--create-debian-package"),n(": creates a .deb package as "),e("code",null,"out/atom-{arch}.deb")]),e("li",null,[e("code",null,"--create-rpm-package"),n(": creates a .rpm package as "),e("code",null,"out/atom-{arch}.rpm")]),e("li",null,[e("code",null,"--install[=dir]"),n(": installs the application in "),e("code",null,"${dir}"),n("; "),e("code",null,"${dir}"),n(" defaults to "),e("code",null,"/usr/local"),n(".")])],-1),rl=e("ul",null,[e("li",null,[e("code",null,"--code-sign"),n(": signs the application with the GitHub certificate specified in "),e("code",null,"$ATOM_MAC_CODE_SIGNING_CERT_DOWNLOAD_URL"),n(".")]),e("li",null,[e("code",null,"--compress-artifacts"),n(": zips the generated application as "),e("code",null,"out/atom-mac.zip"),n(".")]),e("li",null,[e("code",null,"--install[=dir]"),n(": installs the application at "),e("code",null,"${dir}/Atom.app"),n(" for dev and stable versions or at "),e("code",null,"${dir}/Atom-Beta.app"),n(" for beta versions; "),e("code",null,"${dir}"),n(" defaults to "),e("code",null,"/Applications"),n(".")])],-1),pl=e("ul",null,[e("li",null,[e("code",null,"--code-sign"),n(": signs the application with the GitHub certificate specified in "),e("code",null,"$WIN_P12KEY_URL"),n(".")]),e("li",null,[e("code",null,"--compress-artifacts"),n(": zips the generated application as "),e("code",null,"out\\atom-windows.zip"),n(".")]),e("li",null,[e("code",null,"--create-windows-installer"),n(": creates an "),e("code",null,".exe"),n(" and two "),e("code",null,".nupkg"),n(" packages in the "),e("code",null,"out"),n(" directory.")]),e("li",null,[e("code",null,"--install[=dir]"),n(": installs the application in "),e("code",null,"${dir}\\Atom\\app-dev"),n("; "),e("code",null,"${dir}"),n(" defaults to "),e("code",null,"%LOCALAPPDATA%"),n(".")])],-1),dl=e("h5",{id:"troubleshooting",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#troubleshooting","aria-hidden":"true"},"#"),n(" Troubleshooting")],-1),ul=e("h6",{id:"typeerror-unable-to-watch-path",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typeerror-unable-to-watch-path","aria-hidden":"true"},"#"),n(" TypeError: Unable to watch path")],-1),hl=e("p",null,"If you get following error with a big traceback right after Atom starts:",-1),ml=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`TypeError: Unable to watch path +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),gl=e("p",null,"you have to increase number of watched files by inotify. For testing if this is the reason for this error you can execute:",-1),kl=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo sysctl fs.inotify.max_user_watches=32768 +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),vl=e("p",null,"then restart Atom. If Atom now works fine, you can make this setting permanent:",-1),bl=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ echo 32768 | sudo tee -a /proc/sys/fs/inotify/max_user_watches +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),fl={href:"https://github.com/atom/atom/issues/2082",target:"_blank",rel:"noopener noreferrer"},yl=e("h6",{id:"usr-bin-env-node-no-such-file-or-directory",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#usr-bin-env-node-no-such-file-or-directory","aria-hidden":"true"},"#"),n(" /usr/bin/env: node: No such file or directory")],-1),wl=e("p",null,[n("If you get this notice when attempting to run any script, you either do not have Node.js installed, or node isn't identified as Node.js on your machine. If it's the latter, this might be caused by installing Node.js via the distro package manager and not nvm, so entering "),e("code",null,"sudo ln -s /usr/bin/nodejs /usr/bin/node"),n(" into your terminal may fix the issue. On some variants (mostly Debian based distros) you can use "),e("code",null,"update-alternatives"),n(" too:")],-1),_l=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo update-alternatives --install /usr/bin/node node /usr/bin/nodejs 1 --slave /usr/bin/js js /usr/bin/nodejs +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),xl=e("h6",{id:"attributeerror-module-object-has-no-attribute-script-main",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#attributeerror-module-object-has-no-attribute-script-main","aria-hidden":"true"},"#"),n(" AttributeError: 'module' object has no attribute 'script_main'")],-1),ql=e("p",null,"If you get following error with a big traceback while building Atom:",-1),Al=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`sys.exit(gyp.script_main()) AttributeError: 'module' object has no attribute 'script_main' gyp ERR! +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Tl=e("p",null,"you need to uninstall the system version of gyp.",-1),Cl=e("p",null,"On Fedora you would do the following:",-1),Sl=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo yum remove gyp +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),Il=e("h6",{id:"linux-build-error-reports",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#linux-build-error-reports","aria-hidden":"true"},"#"),n(" Linux build error reports")],-1),jl={href:"https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Alinux&type=Issues",target:"_blank",rel:"noopener noreferrer"},Pl={href:"https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Amac&type=Issues",target:"_blank",rel:"noopener noreferrer"},Rl=e("h6",{id:"common-errors",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#common-errors","aria-hidden":"true"},"#"),n(" Common Errors")],-1),Nl=e("li",null,[e("p",null,[e("code",null,"node is not recognized")]),e("ul",null,[e("li",null,[n("If you just installed Node.js, you'll need to restart Command Prompt before the "),e("code",null,"node"),n(" command is available on your path.")])])],-1),El=e("li",null,[e("p",null,[e("code",null,"msbuild.exe failed with exit code: 1")]),e("ul",null,[e("li",null,[n("If using "),e("strong",null,"Visual Studio"),n(", ensure you have the "),e("strong",null,"Visual C++"),n(" component installed. Go into Add/Remove Programs, select Visual Studio, press Modify, and then check the Visual C++ box.")]),e("li",null,[n("If using "),e("strong",null,"Visual C++ Build Tools"),n(", ensure you have the "),e("strong",null,"Windows 8 SDK"),n(" or "),e("strong",null,"Windows 10 SDK"),n(' component installed. Go into Add/Remove Programs, select Visual C++ Build Tools, press Modify and then check the "Windows 8 SDK" or "Windows 10 SDK" box.')])])],-1),Ul=e("p",null,[e("code",null,"script\\build"),n(" stops with no error or warning shortly after displaying the versions of node, npm and Python")],-1),Ml=e("li",null,[n("Make sure that the path where you have checked out Atom does not include a space. For example, use "),e("code",null,"C:\\atom"),n(" instead of "),e("code",null,"C:\\my stuff\\atom"),n(".")],-1),Wl=e("code",null,"C:\\atom",-1),Fl={href:"https://github.com/atom/atom/issues/2200",target:"_blank",rel:"noopener noreferrer"},Dl=e("p",null,[e("code",null,"error MSB4025: The project file could not be loaded. Invalid character in the given encoding.")],-1),Ll=e("code",null,"%USERPROFILE%",-1),Ol={href:"https://bugs.chromium.org/p/gyp/issues/list",target:"_blank",rel:"noopener noreferrer"},zl=e("ul",null,[e("li",null,"https://github.com/TooTallNate/node-gyp/issues/297"),e("li",null,"https://bugs.chromium.org/p/gyp/issues/detail?id=393")],-1),Yl=e("li",null,[e("p",null,[e("code",null,"'node_modules\\.bin\\npm' is not recognized as an internal or external command, operable program or batch file.")]),e("ul",null,[e("li",null,[n("This occurs if the previous build left things in a bad state. Run "),e("code",null,"script\\clean"),n(" and then "),e("code",null,"script\\build"),n(" again.")])])],-1),Vl=e("li",null,[e("p",null,[e("code",null,"script\\build"),n(" stops at installing runas with "),e("code",null,"Failed at the runas@x.y.z install script.")]),e("ul",null,[e("li",null,"See the next item.")])],-1),$l=e("li",null,[e("p",null,[e("code",null,"error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.")]),e("ul",null,[e("li",null,[n("Try setting the "),e("code",null,"GYP_MSVS_VERSION"),n(" environment variable to "),e("strong",null,"2015"),n(" or "),e("strong",null,"2017"),n(" depending on what version of Visual Studio/Build Tools is installed and then "),e("code",null,"script\\clean"),n(" followed by "),e("code",null,"script\\build"),n(" (re-open the Command Prompt if you set the variable using the GUI).")])])],-1),Gl=e("li",null,[e("p",null,[e("code",null,"'node-gyp' is not recognized as an internal or external command, operable program or batch file.")]),e("ul",null,[e("li",null,[n("Try running "),e("code",null,"npm install -g node-gyp"),n(", and run "),e("code",null,"script\\build"),n(" again.")])])],-1),Hl=e("li",null,[e("p",null,[n("Other "),e("code",null,"node-gyp"),n(" errors on first build attempt, even though the right Node.js and Python versions are installed.")]),e("ul",null,[e("li",null,"Do try the build command one more time as experience shows it often works on second try in many cases.")])],-1),Bl=e("h6",{id:"windows-build-error-reports",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#windows-build-error-reports","aria-hidden":"true"},"#"),n(" Windows build error reports")],-1),Jl={href:"https://github.com/atom/atom/search?q=label%3Abuild-error+label%3Awindows&type=Issues",target:"_blank",rel:"noopener noreferrer"},Kl=e("li",null,"If it hasn't, please open a new issue with your Windows version, architecture (x86 or x64), and a text dump of your build output, including the Node.js and Python versions.",-1),Xl=e("h3",{id:"contributing-to-official-atom-packages",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#contributing-to-official-atom-packages","aria-hidden":"true"},"#"),n(" Contributing to Official Atom Packages")],-1),Zl={href:"https://github.com/atom/atom",target:"_blank",rel:"noopener noreferrer"},Ql=e("h4",{id:"hacking-on-packages",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#hacking-on-packages","aria-hidden":"true"},"#"),n(" Hacking on Packages")],-1),ec=e("h5",{id:"cloning",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#cloning","aria-hidden":"true"},"#"),n(" Cloning")],-1),nc=e("code",null,"apm install",-1),ac=t(`For example, if you want to make changes to the tree-view
package, fork the repo on your github account, then clone it:
$ git clone git@github.com:<em>your-username</em>/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ apm install
+> Installing modules \u2713
+
Now you can link it to development mode so when you run an Atom window with atom --dev
, you will use your fork instead of the built in package:
$ apm link -d
+
Editing a package in Atom is a bit of a circular experience: you're using Atom to modify itself. What happens if you temporarily break something? You don't want the version of Atom you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the "Application: Open Dev" command. You can also run dev mode from the command line with atom --dev
.
To load your package in development mode, create a symlink to it in ~/.atom/dev/packages
. This occurs automatically when you clone the package with apm develop
. You can also run apm link --dev
and apm unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running apm update
after pulling any upstream changes.
Unzip the file to a temporary location (for example /tmp/atom
C:\\TEMP\\atom
)
Copy the contents of the desired package into a working directory for your fork
$ <span class='platform-mac platform-linux'>cp -R /tmp/atom/packages/one-light-ui ~/src/one-light-ui-plus</span><span class='platform-windows'>xcopy C:\\TEMP\\atom\\packages\\one-light-ui C:\\src\\one-light-ui-plus /E /H /K</span>
+
Create a local repository and commit the initial contents
$ cd ~/src/one-light-ui-plus
+$ git init
+$ git commit -am "Import core Atom package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
$ git remote add upstream https://github.com/atom/atom.git
+
Tip
Tip: Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the atom/atom repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log upstream/master -- packages/one-light-ui
+8ac9919a0 Bump up border size (Hugh Baht, 17 minutes ago)
+3bf4d226e Remove obsolete build status link in one-light-ui README (Jason Rudolph, 3 days ago)
+3edf64ad0 Merge pull request #42 from atom/sm-select-list (simurai, 2 weeks ago)
+...
+
Look through the log and identify the commits that you want to merge into your fork.
$ git format-patch -1 --stdout 8ac9919a0 | git am -p3
+
Repeat this step for each commit that you want to merge into your fork.
If you finished this chapter, you should be an Atom-hacking master. We've discussed how you should work with CoffeeScript, and how to put it to good use in creating packages. You should also be able to do this in your own created theme now.
Even when something goes wrong, you should be able to debug this easily. But also fewer things should go wrong, because you are capable of writing great specs for Atom.
In the next chapter, we\u2019ll go into more of a deep dive on individual internal APIs and systems of Atom, even looking at some Atom source to see how things are really getting done.
`,6);function Ec(Uc,Mc){const s=d("ExternalLinkIcon"),i=d("RouterLink"),p=d("Tabs");return E(),U("div",null,[F,e("p",null,[n(`Now it's time to come to the "Hackable" part of the Hackable Editor. As we've seen throughout the second section, a huge part of Atom is made up of bundled packages. If you wish to add some functionality to Atom, you have access to the same APIs and tools that the core features of Atom has. From the `),e("a",D,[n("tree-view"),a(s)]),n(" to the "),e("a",L,[n("command-palette"),a(s)]),n(" to "),e("a",O,[n("find-and-replace"),a(s)]),n(" functionality, even the most core features of Atom are implemented as packages.")]),z,e("p",null,[n("We'll go over examples like this in a bit, but this is what the language looks like. Just about everything you can do with CoffeeScript in Atom is also doable in JavaScript. You can brush up on CoffeeScript at "),e("a",Y,[n("coffeescript.org"),a(s)]),n(".")]),e("p",null,[n("Less is an even simpler transition from CSS. It adds a number of useful things like variables and functions to CSS. You can learn about Less at "),e("a",V,[n("lesscss.org"),a(s)]),n(". Our usage of Less won't get too complex in this book however, so as long as you know basic CSS you should be fine.")]),$,e("p",null,[n("When Atom finishes loading, it will evaluate "),G,n(" in your "),H,B,n(" directory, giving you a chance to run CoffeeScript code to make customizations. Code in this file has full access to "),e("a",J,[n("Atom's API"),a(s)]),n(". If customizations become extensive, consider creating a package, which we will cover in "),K,n(".")]),X,e("p",null,[n("Because "),Z,n(" provides access to Atom's API, you can use it to implement useful commands without creating a new package or extending an existing one. Here's a command which uses the "),e("a",Q,[n("Selection API"),a(s)]),n(" and "),e("a",ee,[n("Clipboard API"),a(s)]),n(" to construct a Markdown link from the selected text and the clipboard contents as the URL:")]),ne,e("p",null,[n("Now, reload Atom and use the "),a(i,{to:"/getting-started/sections/atom-basics/#command-palette"},{default:o(()=>[n("Command Palette")]),_:1}),n(` to execute the new command, "Markdown: Paste As Link", by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a `),a(i,{to:"/using-atom/sections/basic-customization/#customizing-keybindings"},{default:o(()=>[n("keybinding for the command")]),_:1}),n(".")]),ae,se,te,e("p",null,[n("The simplest way to start a package is to use the built-in package generator that ships with Atom. As you might expect by now, this generator is itself a separate package implemented in "),e("a",oe,[n("package-generator"),a(s)]),n(".")]),ie,e("div",le,[ce,e("p",null,[re,n(" You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on "),e("a",pe,[n("atom.io"),a(s)]),n(' (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the '),de,n(" package name, you "),ue,n(" be safe \u{1F600}")])]),he,e("p",null,[n("Similar to "),e("a",me,[n("Node modules"),a(s)]),n(", Atom packages contain a "),ge,n(' file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.')]),e("p",null,[n("In addition to some of the regular "),e("a",ke,[n("Node "),ve,n(" keys"),a(s)]),n(" available, Atom "),be,n(" files have their own additions.")]),fe,e("ul",null,[ye,e("li",null,[we,n(": (Available in Atom 1.14 and above) This "),_e,n(" method is similar to "),xe,n(" but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after "),a(i,{to:"/behind-atom/sections/serialization-in-atom/#serialization-methods"},{default:o(()=>[n("your package's deserializers")]),_:1}),n(" have been called), "),qe,n(" is guaranteed to be called before everything. Use "),Ae,n(" if you want to be sure that the workspace is ready; use "),Te,n(" if you need to do some setup prior to your deserializers or view providers being invoked.")]),Ce,Se]),Ie,e("p",null,[n("Style sheets for your package should be placed in the "),je,n(" directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or "),e("a",Pe,[n("Less"),a(s)]),n(", but Less is recommended.")]),Re,e("p",null,[n("If you "),Ne,n(" need special styling, try to keep only structural styles in the package style sheets. If you "),Ee,n(" specify colors and sizing, these should be taken from the active theme's "),e("a",Ue,[n("ui-variables.less"),a(s)]),n(".")]),Me,e("p",null,[n("We'll cover more advanced keybinding stuff a bit later in "),a(i,{to:"/behind-atom/sections/keymaps-in-depth/"},{default:o(()=>[n("Keymaps in Depth")]),_:1}),n(".")]),We,e("p",null,[n("Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling "),e("a",Fe,[De,a(s)]),n(".")]),e("p",null,[n("Next we get the number of words by calling "),e("a",Le,[Oe,a(s)]),n(" on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.")]),ze,e("p",null,[n("Under the hood, "),e("a",Ye,[n("Jasmine v1.3"),a(s)]),n(" executes your tests, so you can assume that any DSL available there is also available to your package.")]),Ve,e("p",null,[n("We've now generated, customized and tested our first plugin for Atom. Congratulations! Now let's go ahead and "),a(i,{to:"/docs/atom-archive/hacking-atom/publishing/"},{default:o(()=>[n("publish")]),_:1}),n(" it so it's available to the world.")]),$e,e("p",null,[n("Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with "),e("a",Ge,[n("ascii art"),a(s)]),n('. When you run our new command with the word "cool" selected, it will be replaced with:')]),He,e("p",null,[n("To begin, press "),Be,Je,n(" to bring up the "),e("a",Ke,[n("Command Palette"),a(s)]),n('. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in '),a(i,{to:"/hacking-atom/sections/package-word-count/#package-generator"},{default:o(()=>[n("the section on package generation")]),_:1}),n(". Enter "),Xe,n(" as the name of the package.")]),Ze,e("p",null,[n("As in "),a(i,{to:"/hacking-atom/sections/package-word-count/#counting-the-words"},{default:o(()=>[n("Counting Words")]),_:1}),n(", we're using "),Qe,n(" to get the object that represents the active text editor. If this "),en,n(" method is called when not focused on a text editor, nothing will happen.")]),e("p",null,[n("Next we insert a string into the current text editor with the "),e("a",nn,[an,a(s)]),n(' method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.')]),sn,e("p",null,[n("Now we need to convert the selected text to ASCII art. To do this we will use the "),e("a",tn,[n("figlet"),a(s)]),n(" Node module from "),e("a",on,[n("npm"),a(s)]),n(". Open "),ln,n(" and add the latest version of figlet to the dependencies:")]),cn,e("p",null,[n("There are a couple of new things in this example we should look at quickly. The first is the "),e("a",rn,[pn,a(s)]),n(" which, as you might guess, returns the text that is currently selected.")]),e("p",null,[n("We then call the Figlet code to convert that into something else and replace the current selection with it with the "),e("a",dn,[un,a(s)]),n(" call.")]),hn,mn,gn,e("p",null,[n("We saw in our "),a(i,{to:"/hacking-atom/sections/package-word-count/"},{default:o(()=>[n("Word Count")]),_:1}),n(" package how we could show information in a modal panel. However, panels aren't the only way to extend Atom's UI\u2014you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Atom will restore them the next time you open the project. This system is used by Atom's tree view, as well as by third party packages like "),e("a",kn,[n("Nuclide"),a(s)]),n(" for its console, debugger, outline view, and diagnostics (linter results).")]),vn,bn,e("p",null,[n("To begin, press "),fn,yn,n(" to bring up the "),e("a",wn,[n("Command Palette"),a(s)]),n('. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in '),a(i,{to:"/hacking-atom/sections/package-word-count/#package-generator"},{default:o(()=>[n("the section on package generation")]),_:1}),n(". Enter "),_n,n(" as the name of the package.")]),xn,e("p",null,[n("Atom's interface is rendered using HTML, and it's styled via "),e("a",qn,[n("Less"),a(s)]),n(" which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.")]),An,e("ul",null,[e("li",null,[n("Less is a superset of CSS, but it has some really handy features like variables. If you aren't familiar with its syntax, take a few minutes to "),e("a",Tn,[n("familiarize yourself"),a(s)]),n(".")]),e("li",null,[n("You may also want to review the concept of a "),Cn,n(" (as covered in "),a(i,{to:"/hacking-atom/sections/package-word-count/#packagejson"},{default:o(()=>[n("Atom "),Sn]),_:1}),n("). This file is used to help distribute your theme to Atom users.")]),In,e("li",null,[n("You can find existing themes to install or fork in "),e("a",jn,[n("the atom.io themes registry"),a(s)]),n(".")])]),Pn,e("ol",null,[e("li",null,[n("Fork the "),e("a",Rn,[n("ui-theme-template"),a(s)])]),Nn]),En,e("ul",null,[e("li",null,[e("a",Un,[n("ui-variables.less"),a(s)])]),e("li",null,[e("a",Mn,[n("syntax-variables.less"),a(s)])])]),Wn,Fn,Dn,e("p",null,[n("Your package should generally only specify structural styling, and these should come from "),e("a",Ln,[n("the style guide"),a(s)]),n(". Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!")]),On,e("p",null,[n("Reloading by pressing "),zn,Yn,n(" after you make changes to your theme is less than ideal. Atom supports "),e("a",Vn,[n("live updating"),a(s)]),n(" of styles on Atom windows in Dev Mode.")]),$n,e("p",null,[n("Check out Google's "),e("a",Gn,[n("extensive tutorial"),a(s)]),n(" for a short introduction.")]),Hn,e("p",null,[n("If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The "),e("a",Bn,[n("Styleguide"),a(s)]),n(" is a page that renders every component Atom supports.")]),Jn,e("p",null,[n("Follow the steps on the "),a(i,{to:"/docs/atom-archive/hacking-atom/publishing/"},{default:o(()=>[n("Publishing")]),_:1}),n(" page. The example used is for the Word Count package, but publishing a theme works exactly the same.")]),Kn,e("p",null,[n("Atom's syntax highlighting and code folding system is powered by "),e("a",Xn,[n("Tree-sitter"),a(s)]),n(". Tree-sitter parsers create and maintain full "),e("a",Zn,[Qn,a(s)]),n(" representing your code.")]),ea,e("p",null,[n("Tree-sitter generates parsers based on "),e("a",na,[n("context-free grammars"),a(s)]),n(" that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Atom.")]),e("p",null,[n("They can also be developed and tested at the command line, separately from Atom. Tree-sitter has "),e("a",aa,[n("its own documentation page"),a(s)]),n(" on how to create these parsers. The "),e("a",sa,[n("Tree-sitter GitHub organization"),a(s)]),n(" also contains a lot of example parsers that you can learn from, each in its own repository.")]),e("p",null,[n("Once you have created a parser, you need to publish it to "),e("a",ta,[n("the NPM registry"),a(s)]),n(" to use it in Atom. To do this, make sure you have a "),oa,n(" and "),ia,n(" in your parser's "),la,n(":")]),ca,e("ul",null,[ra,pa,e("li",null,[da,n(" - The name of the parser node module that will be used for parsing. This string will be passed directly to "),e("a",ua,[ha,a(s)]),n(" in order to load the parser.")]),ma]),ga,e("p",null,[n("Note that in this selector, we're using the "),e("a",ka,[n("immediate child combinator"),a(s)]),n(" ("),va,n("). Arbitrary descendant selectors without this combinator (for example "),ba,n(", which would match any "),fa,n(" occurring anywhere within a "),ya,n(") are currently not supported.")]),wa,e("p",null,[n("You can use the "),e("a",_a,[xa,n(" pseudo-class"),a(s)]),n(" to select nodes based on their order within their parent. For example, this example selects "),qa,n(" nodes which are the fourth (zero-indexed) child of a "),Aa,n(" node.")]),Ta,e("p",null,[n("Finally, you can use double-quoted strings in the selectors to select "),Ca,n(" tokens in the syntax tree, like "),Sa,n(" and "),Ia,n(". See "),e("a",ja,[n("the Tree-sitter documentation"),a(s)]),n(" for more information about named vs anonymous tokens.")]),Pa,e("p",null,[n("If multiple selectors in the "),Ra,n(" object match a node, the node's classes will be decided based on the "),e("a",Na,[n("most specific"),a(s)]),n(" selector. Note that the "),Ea,n(" and "),Ua,n(" rules do "),Ma,n(" affect specificity, so you may need to supply the same "),Wa,n(" or "),Fa,n(" rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:")]),Da,e("p",null,[n("For example, in JavaScript, "),e("a",La,[n("tagged template literals"),a(s)]),n(" sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:")]),Oa,e("ul",null,[e("li",null,[e("a",za,[n("Bash"),a(s)])]),e("li",null,[e("a",Ya,[n("C"),a(s)])]),e("li",null,[e("a",Va,[n("Go"),a(s)])]),e("li",null,[e("a",$a,[n("HTML"),a(s)])]),e("li",null,[e("a",Ga,[n("JavaScript"),a(s)])]),e("li",null,[e("a",Ha,[n("Python"),a(s)])]),e("li",null,[e("a",Ba,[n("Ruby"),a(s)])]),e("li",null,[e("a",Ja,[n("TypeScript"),a(s)])])]),Ka,e("p",null,[n("Grammar files are written in the "),e("a",Xa,[n("CSON"),a(s)]),n(" or "),e("a",Za,[n("JSON"),a(s)]),n(" format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.")]),Qa,e("p",null,[n("The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the "),es,n(", "),ns,n(", "),as,n(", and "),ss,n(" folders. Furthermore, in "),ts,n(", remove the "),os,n(" section. Now create a new folder called "),is,n(", and inside that a file called "),ls,n(". This is the main file that we will be working with - start by populating it with a "),e("a",cs,[n("boilerplate template"),a(s)]),n(". Now let's go over what each key means.")]),rs,e("p",null,[ps,n(" is where your regex is contained, and "),ds,n(" is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in "),e("a",us,[n("Section 12.4 of the TextMate Manual"),a(s)]),n(".")]),hs,e("ul",null,[e("li",null,[e("a",ms,[n("DamnedScholar's Gist"),a(s)]),n(". Provides a template of most keys, each with a short comment explaining their function.")]),e("li",null,[e("a",gs,[n("Aerijo's Gist"),a(s)]),n(". [Work in Progress] Another guide that attempts to fully explain making a grammar package for users of all levels.")]),ks,e("li",null,[e("a",vs,[n("Oniguruma docs"),a(s)]),n(". The documentation for the regex engine Atom uses.")]),e("li",null,[e("a",bs,[n("TextMate Section 12"),a(s)]),n(". Atom uses the same principles as laid out here, including the list of acceptable scopes.")]),e("li",null,[e("a",fs,[ys,a(s)]),n(". Not necessary to write a grammar, but a good technical reference for what Atom is doing behind the scenes.")]),e("li",null,[n("Look at any existing packages, such as the ones for "),e("a",ws,[n("Python"),a(s)]),n(", "),e("a",_s,[n("JavaScript"),a(s)]),n(", "),e("a",xs,[n("HTML"),a(s)]),n(", "),e("a",qs,[n("and more"),a(s)]),n(".")])]),M(` +TODO: +* \`repository\` and including from repository patterns +* Wrap-up +* Implement Package Generator functionality to generate a grammar +* Intermediate + advanced grammar tutorials +`),As,e("p",null,[n("Atom bundles a command line utility called "),Ts,n(" which we first used back in "),a(i,{to:"/using-atom/sections/atom-packages/#command-line"},{default:o(()=>[n("Command Line")]),_:1}),n(" to search for and install packages via the command line. The "),Cs,n(" command can also be used to publish Atom packages to the public registry and update them.")]),Ss,Is,e("ul",null,[js,e("li",null,[n("Your package is in a Git repository that has been pushed to "),e("a",Ps,[n("GitHub"),a(s)]),n(". Follow "),e("a",Rs,[n("this guide"),a(s)]),n(" if your package isn't already on GitHub.")])]),Ns,e("p",null,[n("Before you publish a package it is a good idea to check ahead of time if a package with the same name has already been published to "),e("a",Es,[n("the atom.io package registry"),a(s)]),n(". You can do that by visiting "),Us,n(" to see if the package already exists. If it does, update your package's name to something that is available before proceeding.")]),Ms,e("ol",null,[Ws,Fs,e("li",null,[n("Creates a new "),e("a",Ds,[n("Git tag"),a(s)]),n(" for the version being published.")]),Ls,Os]),zs,e("p",null,[n("If this is the first package you are publishing, the "),Ys,n(" command may prompt you for your GitHub username and password. If you have two-factor authentication enabled, use a "),e("a",Vs,[n("personal access token"),a(s)]),n(" in lieu of a password. This is required to publish and you only need to enter this information the first time you publish. The credentials are stored securely in your "),e("a",$s,[n("keychain"),a(s)]),n(" once you login.")]),Gs,e("p",null,[n("Use "),Hs,n(" when you make a change that breaks backwards compatibility, like changing defaults or removing features. Use "),Bs,n(" when adding new functionality or options, but without breaking backwards compatibility. Use "),Js,n(" when you've changed the implementation of existing features, but without changing the behaviour or options of your package. Check out "),e("a",Ks,[n("semantic versioning"),a(s)]),n(" to learn more about best practices for versioning your package releases.")]),Xs,Zs,e("p",null,[n("Atom comes bundled with the "),e("a",Qs,[n("Octicons 4.4.0"),a(s)]),n(" icon set. Use them to add icons to your packages.")]),et,nt,e("p",null,[n("In the "),a(i,{to:"/hacking-atom/sections/creating-a-theme/#atom-styleguide"},{default:o(()=>[n("Styleguide")]),_:1}),n(` under the "Icons" section you'll find all the Octicons that are available.`)]),at,e("p",null,[n("Although icons can make your UI visually appealing, when used without a text label, it can be hard to guess its meaning. In cases where space for a text label is insufficient, consider adding a "),e("a",st,[n("tooltip"),a(s)]),n(" that appears on hover. Or a more subtle "),tt,n(" attribute would help as well.")]),ot,e("p",null,[n("Atom provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when "),e("a",it,[n("submitting issues"),a(s)]),n(":")]),lt,e("p",null,[n("Then check for the "),e("a",ct,[n("latest Stable version"),a(s)]),n(".")]),a(p,{id:"1675",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"Updating"},{tab0:o(({title:l,value:c,isActive:r})=>[e("p",null,[n("To update to the latest version, you can download it from "),e("a",rt,[n("the atom.io website"),a(s)]),n(" or "),e("a",pt,[n("the latest release on GitHub"),a(s)]),n(" and follow the "),a(i,{to:"/getting-started/sections/installing-atom/#installing-atom-on-linux"},{default:o(()=>[n("Installation instructions for Atom on Linux")]),_:1}),n(".")])]),tab1:o(({title:l,value:c,isActive:r})=>[e("p",null,[n("If there is a more recent release available, you can update to the most recent release with the auto-update functionality built in to Atom and the "),e("a",dt,[n("about package"),a(s)]),n(". You can open the About View by using the "),ut,n(' menu option to see whether Atom is up-to-date, downloading a new update or click the button to "Restart and Install Update".')])]),tab2:o(({title:l,value:c,isActive:r})=>[e("p",null,[n("If there is a more recent release available, you can update to the most recent release with the auto-update functionality built in to Atom and the "),e("a",ht,[n("about package"),a(s)]),n(". You can open the About View by using the "),mt,n(' menu option to see whether Atom is up-to-date, downloading a new update or click the button to "Restart and Install Update".')])]),_:1}),e("p",null,[n("If you're building Atom from source, pull down the latest version of master and "),e("a",gt,[n("re-build"),a(s)]),n(".")]),kt,a(p,{id:"1735",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"Debugging"},{tab0:o(({title:l,value:c,isActive:r})=>[vt]),tab1:o(({title:l,value:c,isActive:r})=>[bt]),tab2:o(({title:l,value:c,isActive:r})=>[ft]),_:1}),yt,e("p",null,[n("If you have packages installed that use native Node modules, when you upgrade to a new version of Atom, they might need to be rebuilt. Atom detects this and through the "),e("a",wt,[n("incompatible-packages package"),a(s)]),n(" displays an indicator in the status bar when this happens.")]),_t,xt,qt,At,e("p",null,[n("Open Atom's "),e("a",Tt,[n("Settings View"),a(s)]),n(" with "),Ct,St,n(", the "),It,jt,Pt,n(' menu option, or the "Settings View: Open" command from the '),e("a",Rt,[n("Command Palette"),a(s)]),n(".")]),Nt,e("p",null,[n("Check Atom's settings in the Settings View, there's a description of most configuration options in the "),a(i,{to:"/using-atom/sections/basic-customization/#configuration-key-reference"},{default:o(()=>[n("Basic Customization section")]),_:1}),n('. For example, if you want Atom to hide the invisible symbols representing whitespace characters, disable the "Show Invisibles" option.')]),Et,e("p",null,[n("Since Atom ships with a set of packages and you can also install additional packages yourself, check the list of packages and their settings. For instance, if you'd like to get rid of the vertical line in the middle of the editor, disable the "),e("a",Ut,[n("Wrap Guide package"),a(s)]),n(". And if you don't like it when Atom strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the "),e("a",Mt,[n("whitespace package's"),a(s)]),n(" settings.")]),Wt,Ft,e("p",null,[n("You might have defined some custom styles, keymaps or snippets in "),a(i,{to:"/using-atom/sections/basic-customization/"},{default:o(()=>[n("one of your configuration files")]),_:1}),n(". In some situations, these personal hacks might be causing the unexpected behavior you're observing so try clearing those files and restarting Atom.")]),Dt,e("p",null,[n("If a command is not executing when you press a key combination or the wrong command is executing, there might be an issue with the keybinding for that combination. Atom ships with the "),e("a",Lt,[n("Keybinding Resolver"),a(s)]),n(", a neat package which helps you understand what key Atom saw you press and the command that was triggered because of it.")]),Ot,e("p",null,[n("If multiple keybindings are matched, Atom determines which keybinding will be executed based on the "),a(i,{to:"/behind-atom/sections/keymaps-in-depth/#specificity-and-cascade-order"},{default:o(()=>[n("specificity of the selectors and the order in which they were loaded")]),_:1}),n(". If the command you wanted to trigger is listed in the Keybinding Resolver, but wasn't the one that was executed, this is normally explained by one of two causes:")]),zt,e("p",null,[n("Atom loads core Atom keybindings and package keybindings first, and user-defined keybindings last. Since user-defined keybindings are loaded last, you can use your "),Yt,n(" file to tweak the keybindings and sort out problems like these. See the "),a(i,{to:"/behind-atom/sections/keymaps-in-depth/"},{default:o(()=>[n("Keymaps in Depth section")]),_:1}),n(" for more information.")]),e("p",null,[n("If you notice that a package's keybindings are taking precedence over core Atom keybindings, it might be a good idea to report the issue on that package's GitHub repository. You can contact atom maintainers on "),e("a",Vt,[n("Atom's github discussions"),a(s)])]),$t,e("p",null,[n("You can determine which fonts are being used to render a specific piece of text by using the Developer Tools. To open the Developer Tools press "),Gt,Ht,n('. Once the Developer Tools are open, click the "Elements" tab. Use the '),e("a",Bt,[n("standard tools for finding the element"),a(s)]),n(' containing the text you want to check. Once you have selected the element, you can click the "Computed" tab in the styles pane and scroll to the bottom. The list of fonts being used will be shown there:')]),Jt,a(p,{id:"1912",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"Debugging"},{tab0:o(({title:l,value:c,isActive:r})=>[Kt]),tab1:o(({title:l,value:c,isActive:r})=>[Xt,Zt,Qt]),tab2:o(({title:l,value:c,isActive:r})=>[eo]),_:1}),no,e("p",null,[n("If Atom is taking a long time to start, you can use the "),e("a",ao,[n("Timecop package"),a(s)]),n(" to get insight into where Atom spends time while loading.")]),so,to,oo,io,lo,e("p",null,[n("If you're experiencing performance problems in a particular situation, your "),e("a",co,[n("Issue reports"),a(s)]),n(" will be more valuable if you include a saved profile from Chrome's CPU profiler that gives some insight into what is slow.")]),ro,e("p",null,[n("To learn more, check out the "),e("a",po,[n("Chrome documentation on CPU profiling"),a(s)]),n(".")]),uo,e("p",null,[n("Check out the pre-requisites in the "),e("a",ho,[n("build instructions"),a(s)]),n(" for your platform for more details.")]),mo,e("p",null,[n("Atom uses "),e("a",go,[n("Jasmine"),a(s)]),n(" as its spec framework. Any new functionality should have specs to guard against regressions.")]),ko,e("p",null,[e("a",vo,[n("Atom specs"),a(s)]),n(" and "),e("a",bo,[n("package specs"),a(s)]),n(" are added to their respective "),fo,n(" directory. The example below creates a spec for Atom core.")]),yo,e("p",null,[n("The best way to learn about expectations is to read the "),e("a",wo,[n("Jasmine documentation"),a(s)]),n(" about them. Below is a simple example.")]),_o,e("ul",null,[e("li",null,[e("a",xo,[n("jasmine-jquery"),a(s)])]),qo]),e("p",null,[n("These are defined in "),e("a",Ao,[n("spec/spec-helper.coffee"),a(s)]),n(".")]),To,e("p",null,[n("For a more detailed documentation on asynchronous tests please visit the "),e("a",Co,[n("Jasmine documentation"),a(s)]),n(".")]),So,e("p",null,[n("It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the "),e("a",Io,[n("Travis CI For Your Packages"),a(s)]),n(" and "),e("a",jo,[n("AppVeyor CI For Your Packages"),a(s)]),n(" posts for more details.")]),Po,e("p",null,[n("Now that we've told Atom that we want our package to handle URIs beginning with "),Ro,n(" via our "),No,n(" method, we need to actually write this method. Atom passes two arguments to your URI handler method; the first one is the fully-parsed URI plus query string, "),e("a",Eo,[n("parsed with Node's "),Uo,a(s)]),n(". The second argument is the raw, string URI; this is normally not needed since the first argument gives you structured information about the URI.")]),Mo,e("p",null,[n("Creation and deletion operations may take a few milliseconds to complete. If you need to remove many files and folders consider "),e("a",Wo,[n("RimRAF"),a(s)]),n(" which has built-in retry logic for this.")]),Fo,e("p",null,[n("It's possible that you have themes or grammars from "),e("a",Do,[n("TextMate"),a(s)]),n(" that you like and use and would like to convert to Atom. If so, you're in luck because there are tools to help with the conversion.")]),Lo,Oo,e("p",null,[n("Let's convert the TextMate bundle for the "),e("a",zo,[n("R"),a(s)]),n(" programming language. You can find other existing TextMate bundles "),e("a",Yo,[n("on GitHub"),a(s)]),n(".")]),Vo,e("p",null,[n("This section will go over how to convert a "),e("a",$o,[n("TextMate"),a(s)]),n(" theme to an Atom theme.")]),Go,e("p",null,[n("TextMate themes use "),e("a",Ho,[n("plist"),a(s)]),n(" files while Atom themes use "),e("a",Bo,[n("CSS"),a(s)]),n(" or "),e("a",Jo,[n("Less"),a(s)]),n(" to style the UI and syntax in the editor.")]),Ko,Xo,e("p",null,[n("Download the theme you wish to convert, you can browse existing TextMate themes on the "),e("a",Zo,[n("TextMate website"),a(s)]),n(".")]),Qo,e("p",null,[n("Follow the "),e("a",ei,[n("GitHub Help instructions on how to fork a repo"),a(s)]),n(".")]),ni,a(p,{id:"2629",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:o(({title:l,value:c,isActive:r})=>[ai]),tab1:o(({title:l,value:c,isActive:r})=>[si]),tab2:o(({title:l,value:c,isActive:r})=>[ti]),_:1}),oi,e("ol",null,[ii,e("li",null,[n("Packages that contain stylesheets, such as syntax themes, will have those stylesheets automatically reloaded by the "),e("a",li,[n("dev-live-reload"),a(s)]),n(" package. This does not live reload JavaScript or CoffeeScript files \u2014 you'll need to reload the window ("),ci,n(") to see changes to those.")])]),ri,a(p,{id:"2685",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:o(({title:l,value:c,isActive:r})=>[pi,di,e("ul",null,[ui,hi,mi,e("li",null,[n("Node.js 10.12 or later (we recommend installing it via "),e("a",gi,[n("nvm"),a(s)]),n(")")]),ki,vi,e("li",null,[n("Development headers for "),e("a",bi,[n("libsecret"),a(s)]),n(".")])]),fi,yi,wi,_i,xi,qi,Ai,Ti,Ci,Si,Ii,ji,Pi]),tab1:o(({title:l,value:c,isActive:r})=>[Ri,e("ul",null,[Ni,e("li",null,[n("Node.js 10.12 or later (we recommend installing it via "),e("a",Ei,[n("nvm"),a(s)]),n(")")]),Ui,Mi,e("li",null,[n("Command Line Tools for "),e("a",Wi,[n("Xcode"),a(s)]),n(" (run "),Fi,n(" to install)")])])]),tab2:o(({title:l,value:c,isActive:r})=>[e("ul",null,[Di,Li,e("li",null,[Oi,e("ul",null,[e("li",null,[e("a",zi,[n("Get Python from the Microsoft Store"),a(s)]),n(", or")]),Yi])]),e("li",null,[Vi,e("ul",null,[e("li",null,[$i,n(),e("a",Gi,[n("windows-build-tools"),a(s)]),n(' - From an elevated Powershell window (right click and "run as Administrator") do: '),Hi,n(" to install")]),e("li",null,[Bi,n(),e("a",Ji,[n("Visual C++ Build Tools 2015 or 2017"),a(s)])]),e("li",null,[Ki,n(),e("a",Xi,[n("Visual Studio 2015 or 2017"),a(s)]),n(" (Community Edition or better)")])]),Zi,Qi])])]),_:1}),el,a(p,{id:"2907",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:o(({title:l,value:c,isActive:r})=>[nl,al]),tab1:o(({title:l,value:c,isActive:r})=>[sl,tl]),tab2:o(({title:l,value:c,isActive:r})=>[ol,il]),_:1}),ll,a(p,{id:"2930",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:o(({title:l,value:c,isActive:r})=>[cl]),tab1:o(({title:l,value:c,isActive:r})=>[rl]),tab2:o(({title:l,value:c,isActive:r})=>[pl]),_:1}),dl,a(p,{id:"3002",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"hacking-atom"},{tab0:o(({title:l,value:c,isActive:r})=>[ul,hl,ml,gl,kl,vl,bl,e("p",null,[n("See also "),e("a",fl,[n("#2082"),a(s)]),n(".")]),yl,wl,_l,xl,ql,Al,Tl,Cl,Sl,Il,e("p",null,[n("Use "),e("a",jl,[n("this search"),a(s)]),n(" to get a list of reports about build errors on Linux.")])]),tab1:o(({title:l,value:c,isActive:r})=>[e("p",null,[n("Use "),e("a",Pl,[n("this search"),a(s)]),n(" to get a list of reports about build errors on macOS.")])]),tab2:o(({title:l,value:c,isActive:r})=>[Rl,e("ul",null,[Nl,El,e("li",null,[Ul,e("ul",null,[Ml,e("li",null,[n("Try moving the repository to "),Wl,n(". Most likely, the path is too long. See "),e("a",Fl,[n("issue #2200"),a(s)]),n(".")])])]),e("li",null,[Dl,e("ul",null,[e("li",null,[n("This can occur because your home directory ("),Ll,n(") has non-ASCII characters in it. This is a bug in "),e("a",Ol,[n("gyp"),a(s)]),n(" which is used to build native Node.js modules and there is no known workaround. "),zl])])]),Yl,Vl,$l,Gl,Hl]),Bl,e("ul",null,[e("li",null,[n("If all else fails, use "),e("a",Jl,[n("this search"),a(s)]),n(" to get a list of reports about build errors on Windows, and see if yours has already been reported.")]),Kl])]),_:1}),Xl,e("p",null,[n("If you think you know which package is causing the issue you are reporting, feel free to open up the issue in that specific repository instead. When in doubt just open the issue on the "),e("a",Zl,[n("atom/atom"),a(s)]),n(" repository but be aware that it may get closed and reopened in the proper package's repository.")]),Ql,ec,e("p",null,[n("The first step is creating your own clone. For some packages, you may also need to install the "),a(i,{to:"/hacking-atom/sections/hacking-on-atom-core/#building"},{default:o(()=>[n("requirements necessary for building Atom")]),_:1}),n(" in order to run "),nc,n(".")]),ac,e("p",null,[n("Several of Atom's core packages are maintained in the "),e("a",sc,[tc,n(" directory of the atom/atom repository"),a(s)]),n(". If you would like to use one of these packages as a starting point for your own package, please follow the steps below.")]),e("div",oc,[ic,e("p",null,[lc,n(" In most cases, we recommend "),a(i,{to:"/docs/atom-archive/hacking-atom/package-word-count/#package-generator"},{default:o(()=>[n("generating a brand new package")]),_:1}),n(" or a "),a(i,{to:"/docs/atom-archive/hacking-atom/creating-a-theme/#creating-a-syntax-theme"},{default:o(()=>[n("brand new theme")]),_:1}),n(" as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Atom package.")])]),cc,e("p",null,[n("For the sake of this guide, let's assume that you want to start with the current code in the "),e("a",rc,[n("one-light-ui"),a(s)]),n(' package, make some customizations to it, and publish your new package under the name "one-light-ui-plus".')]),e("ol",null,[e("li",null,[e("p",null,[n("Download the "),e("a",pc,[n("current contents of the atom/atom repository as a zip file"),a(s)])])]),dc,e("li",null,[e("p",null,[e("a",uc,[n("Create a public repository on github.com"),a(s)]),n(" for your new package")])]),hc,e("li",null,[e("p",null,[n("Follow the steps in the "),a(i,{to:"/docs/atom-archive/hacking-atom/sections/publishing/"},{default:o(()=>[n("Publishing guide")]),_:1}),n(" to publish your new package")])])]),mc,e("p",null,[n("The code in the original package will continue to evolve over time, either to fix bugs or to add new enhancements. You may want to incorporate some or all of those updates into your package. To do so, you can follow "),a(i,{to:"/docs/atom-archive/hacking-atom/sections/maintaining-a-fork-of-a-core-package-in-atom-atom/#step-by-step-guide"},{default:o(()=>[n("these steps")]),_:1}),n(" for merging upstream changes into your package.")]),gc,e("p",null,[n("Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team "),e("a",kc,[n("consolidated many core Atom packages"),a(s)]),n(" into the "),e("a",vc,[n("atom/atom repository"),a(s)]),n(". For example, the one-light-ui package was originally maintained in the "),e("a",bc,[n("atom/one-light-ui"),a(s)]),n(" repository, but it is now maintained in the "),e("a",fc,[yc,n(" directory in the atom/atom repository"),a(s)]),n(".")]),wc,_c,e("p",null,[n("For the sake of this guide, let's assume that you forked the "),e("a",xc,[n("atom/one-light-ui"),a(s)]),n(" repository, renamed your fork to "),qc,n(", and made some customizations.")]),Ac,e("p",null,[n("Add the "),e("a",Tc,[n("atom/atom repository"),a(s)]),n(" as a git remote:")]),Cc,e("p",null,[n("For each commit that you want to bring into your fork, use "),e("a",Sc,[Ic,a(s)]),n(" in conjunction with "),e("a",jc,[Pc,a(s)]),n(". For example, to merge commit "),Rc,n(" into your fork:")]),Nc])}const Yc=N(W,[["render",Ec],["__file","index.html.vue"]]);export{Yc as default}; diff --git a/assets/index.html.563e9153.js b/assets/index.html.563e9153.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.563e9153.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.576a4614.js b/assets/index.html.576a4614.js new file mode 100644 index 0000000000..4c7834efab --- /dev/null +++ b/assets/index.html.576a4614.js @@ -0,0 +1 @@ +import{_ as a,o as s,c as t,a as e,b as o}from"./app.0e1565ce.js";const r={},c=e("h1",{id:"glossary",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#glossary","aria-hidden":"true"},"#"),o(" Glossary")],-1),n=[c];function _(d,i){return s(),t("div",null,n)}const h=a(r,[["render",_],["__file","index.html.vue"]]);export{h as default}; diff --git a/assets/index.html.58a10652.js b/assets/index.html.58a10652.js new file mode 100644 index 0000000000..6fb4511a0c --- /dev/null +++ b/assets/index.html.58a10652.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-36dcd4ed","path":"/docs/atom-archive/upgrading-to-1-0-apis/","title":"Appendix D : Upgrading to 1.0 APIs","lang":"en-US","frontmatter":{"title":"Appendix D : Upgrading to 1.0 APIs","sitemap":{"priority":0.1}},"excerpt":"","headers":[{"level":2,"title":"Upgrading to 1.0 APIs","slug":"upgrading-to-1-0-apis","link":"#upgrading-to-1-0-apis","children":[{"level":3,"title":"Upgrading Your Package","slug":"upgrading-your-package","link":"#upgrading-your-package","children":[]},{"level":3,"title":"Upgrading Your UI Theme Or Package Selectors","slug":"upgrading-your-ui-theme-or-package-selectors","link":"#upgrading-your-ui-theme-or-package-selectors","children":[]},{"level":3,"title":"Upgrading Your Syntax Theme","slug":"upgrading-your-syntax-theme","link":"#upgrading-your-syntax-theme","children":[]}]}],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":0.56,"words":167},"filePathRelative":"docs/atom-archive/upgrading-to-1-0-apis/index.md"}');export{e as data}; diff --git a/assets/index.html.59090f4c.js b/assets/index.html.59090f4c.js new file mode 100644 index 0000000000..dd813ef89b --- /dev/null +++ b/assets/index.html.59090f4c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-35eded6e","path":"/docs/launch-manual/sections/getting-started/","title":"Getting Started","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Why Pulsar?","slug":"why-pulsar","link":"#why-pulsar","children":[{"level":3,"title":"The Nucleus of Pulsar","slug":"the-nucleus-of-pulsar","link":"#the-nucleus-of-pulsar","children":[]},{"level":3,"title":"The Native Web","slug":"the-native-web","link":"#the-native-web","children":[]},{"level":3,"title":"JavaScript, Meet C++","slug":"javascript-meet-c","link":"#javascript-meet-c","children":[]},{"level":3,"title":"Web Tech: The Fun Parts","slug":"web-tech-the-fun-parts","link":"#web-tech-the-fun-parts","children":[]},{"level":3,"title":"An Open-Source Text Editor","slug":"an-open-source-text-editor","link":"#an-open-source-text-editor","children":[]}]},{"level":2,"title":"Installing Pulsar","slug":"installing-pulsar","link":"#installing-pulsar","children":[{"level":3,"title":"Universal releases","slug":"universal-releases","link":"#universal-releases","children":[]},{"level":3,"title":"Debian/Ubuntu based distributions","slug":"debian-ubuntu-based-distributions","link":"#debian-ubuntu-based-distributions","children":[]},{"level":3,"title":"Fedora/RHEL based distributions","slug":"fedora-rhel-based-distributions","link":"#fedora-rhel-based-distributions","children":[]},{"level":3,"title":"Updating Pulsar","slug":"updating-pulsar","link":"#updating-pulsar","children":[]},{"level":3,"title":"Portable Mode","slug":"portable-mode","link":"#portable-mode","children":[]},{"level":3,"title":"Building Pulsar from Source","slug":"building-pulsar-from-source","link":"#building-pulsar-from-source","children":[]},{"level":3,"title":"Proxy and Firewall Settings","slug":"proxy-and-firewall-settings","link":"#proxy-and-firewall-settings","children":[]}]},{"level":2,"title":"Pulsar Basics","slug":"pulsar-basics","link":"#pulsar-basics","children":[{"level":3,"title":"Terminology","slug":"terminology","link":"#terminology","children":[]},{"level":3,"title":"Command Palette","slug":"command-palette","link":"#command-palette","children":[]},{"level":3,"title":"Settings and Preferences","slug":"settings-and-preferences","link":"#settings-and-preferences","children":[]},{"level":3,"title":"Opening, Modifying, and Saving Files","slug":"opening-modifying-and-saving-files","link":"#opening-modifying-and-saving-files","children":[]},{"level":3,"title":"Opening Directories","slug":"opening-directories","link":"#opening-directories","children":[]}]},{"level":2,"title":"Summary","slug":"summary","link":"#summary","children":[]}],"git":{"updatedTime":1672104919000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.26,"words":78},"filePathRelative":"docs/launch-manual/sections/getting-started/index.md"}');export{e as data}; diff --git a/assets/index.html.5925a712.js b/assets/index.html.5925a712.js new file mode 100644 index 0000000000..1aa4fc8e5c --- /dev/null +++ b/assets/index.html.5925a712.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-65ee6ad2","path":"/category/dev/","title":"dev Category","lang":"en-US","frontmatter":{"title":"dev Category","blog":{"type":"category","name":"dev","key":"category"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.5cd06018.js b/assets/index.html.5cd06018.js new file mode 100644 index 0000000000..cefb93701d --- /dev/null +++ b/assets/index.html.5cd06018.js @@ -0,0 +1 @@ +import{_ as o,o as r,c as l,a as t,b as e,d as n,r as i}from"./app.0e1565ce.js";const c={},s=t("h1",{id:"atom-languageclient",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#atom-languageclient","aria-hidden":"true"},"#"),e(" Atom-LanguageClient")],-1),h=t("p",null,[e("Welcome to the "),t("code",null,"atom-languageclient"),e(" wiki!")],-1),u={href:"https://github.com/atom/atom-languageclient/wiki",target:"_blank",rel:"noopener noreferrer"},m={href:"https://github.com/pulsar-edit/atom-languageclient",target:"_blank",rel:"noopener noreferrer"};function _(d,g){const a=i("ExternalLinkIcon");return r(),l("div",null,[s,h,t("p",null,[e("This wiki has been nearly directly taken from the "),t("a",u,[e("upstream Atom Wiki"),n(a)]),e(". That may mean that parts of it are out of date, but otherwise hopefully is a helpful resource relating to our "),t("a",m,[e("Atom-LanguageClient Repo"),n(a)]),e("!")])])}const f=o(c,[["render",_],["__file","index.html.vue"]]);export{f as default}; diff --git a/assets/index.html.5de84039.js b/assets/index.html.5de84039.js new file mode 100644 index 0000000000..d5d230b970 --- /dev/null +++ b/assets/index.html.5de84039.js @@ -0,0 +1,2 @@ +import{_ as n,o as r,c as s,e as i,a as e,b as t,d as o,r as d}from"./app.0e1565ce.js";const c={},l=e("h1",{id:"pulsar-tooling",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#pulsar-tooling","aria-hidden":"true"},"#"),t(" Pulsar tooling")],-1),h=e("p",null,"Here you will find a list of tools used by the Pulsar team and information about them.",-1),u=e("h2",{id:"continuous-integration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#continuous-integration","aria-hidden":"true"},"#"),t(" Continuous Integration")],-1),p={id:"cirrus-ci",tabindex:"-1"},_=e("a",{class:"header-anchor",href:"#cirrus-ci","aria-hidden":"true"},"#",-1),g={href:"https://cirrus-ci.com/github/pulsar-edit/pulsar",target:"_blank",rel:"noopener noreferrer"},f=e("p",null,"Cirrus CI is used for Pulsar's continuous integration as well as for building application binaries.",-1),b={id:"codacy",tabindex:"-1"},m=e("a",{class:"header-anchor",href:"#codacy","aria-hidden":"true"},"#",-1),k={href:"https://app.codacy.com/gh/pulsar-edit/repositories",target:"_blank",rel:"noopener noreferrer"},y=e("p",null,"Codacy is used to scan committed code for any issues that may have been missed.",-1),x=e("p",null,"Currently though Codacy is only used on the following repositories:",-1),C={href:"https://app.codacy.com/gh/pulsar-edit/ppm/dashboard",target:"_blank",rel:"noopener noreferrer"},P={href:"https://app.codacy.com/gh/pulsar-edit/pulsar/dashboard",target:"_blank",rel:"noopener noreferrer"},w={href:"https://app.codacy.com/gh/pulsar-edit/background-tips/dashboard",target:"_blank",rel:"noopener noreferrer"},v={href:"https://app.codacy.com/gh/pulsar-edit/autocomplete-plus/dashboard",target:"_blank",rel:"noopener noreferrer"},T=e("h2",{id:"i18n-internationalization",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#i18n-internationalization","aria-hidden":"true"},"#"),t(" i18n (Internationalization)")],-1),A={id:"crowdin",tabindex:"-1"},I=e("a",{class:"header-anchor",href:"#crowdin","aria-hidden":"true"},"#",-1),D={href:"https://crowdin.pulsar-edit.dev/",target:"_blank",rel:"noopener noreferrer"},E=e("p",null,"Crowdin will be used for Pulsar's internationalization efforts but exact details on this are still pending.",-1),N=e("h2",{id:"package-managers",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#package-managers","aria-hidden":"true"},"#"),t(" Package Managers")],-1),z=e("p",null,"While most repositories you can easily tell what Package Manager is being used by checking for a specific lock file, there are some execptions to this that should be noted.",-1),B={href:"https://github.com/pulsar-edit/pulsar",target:"_blank",rel:"noopener noreferrer"},G=e("code",null,"yarn",-1),L={href:"https://github.com/pulsar-edit/pulsar-edit.github.io",target:"_blank",rel:"noopener noreferrer"},M=e("code",null,"pnpm",-1),V=e("h2",{id:"cloud-database",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#cloud-database","aria-hidden":"true"},"#"),t(" Cloud Database")],-1),H={href:"https://github.com/pulsar-edit/package-backend",target:"_blank",rel:"noopener noreferrer"},O=e("h2",{id:"cloud-compute",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#cloud-compute","aria-hidden":"true"},"#"),t(" Cloud Compute")],-1),S={href:"https://github.com/pulsar-edit/package-backend",target:"_blank",rel:"noopener noreferrer"},Q={href:"https://github.com/pulsar-edit/package-frontend",target:"_blank",rel:"noopener noreferrer"},R=e("h2",{id:"additional-testing-tools",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#additional-testing-tools","aria-hidden":"true"},"#"),t(" Additional Testing tools")],-1),W={id:"action-pulsar-dependency-tester",tabindex:"-1"},j=e("a",{class:"header-anchor",href:"#action-pulsar-dependency-tester","aria-hidden":"true"},"#",-1),q={href:"https://github.com/marketplace/actions/action-pulsar-dependency-tester",target:"_blank",rel:"noopener noreferrer"},F=e("p",null,"Pulsar Dependency Tester is a GitHub action used to test changes of a core Pulsar Dependency if you need to determine how your core package dependency will run on the current version of the Pulsar Editor.",-1),J={href:"https://github.com/marketplace/actions/setup-atom",target:"_blank",rel:"noopener noreferrer"};function K(U,X){const a=d("ExternalLinkIcon");return r(),s("div",null,[i(`This page is generated from the TOOLING.md page on the org-level +documentation at https://github.com/pulsar-edit/.github`),l,h,u,e("h3",p,[_,t(),e("a",g,[t("Cirrus CI"),o(a)])]),f,e("h3",b,[m,t(),e("a",k,[t("Codacy"),o(a)])]),y,x,e("ul",null,[e("li",null,[e("a",C,[t("ppm"),o(a)])]),e("li",null,[e("a",P,[t("pulsar"),o(a)])]),e("li",null,[e("a",w,[t("background-tips"),o(a)])]),e("li",null,[e("a",v,[t("autocomplete-plus"),o(a)])])]),T,e("h3",A,[I,t(),e("a",D,[t("Crowdin"),o(a)])]),E,N,z,e("ul",null,[e("li",null,[e("a",B,[t("pulsar"),o(a)]),t(" uses "),G,t(" as its Package Manager.")]),e("li",null,[e("a",L,[t("pulsar-edit.github.io"),o(a)]),t(" uses "),M,t(" as its Package Manager.")])]),V,e("p",null,[t("The "),e("a",H,[t("package-backend"),o(a)]),t(" currently uses DigitalOcean to host the PostgreSQL Pulsar Package Repositories data in the cloud.")]),O,e("p",null,[t("Both the "),e("a",S,[t("package-backend"),o(a)]),t(" and "),e("a",Q,[t("package-frontend"),o(a)]),t(" use Google App Engine to host the compute instance of these websites in the cloud.")]),R,e("h3",W,[j,t(),e("a",q,[t("Action Pulsar Dependency Tester"),o(a)])]),F,e("p",null,[t("This GitHub Action is a direct Pulsar replacement of the previous Action to test Atom Dependencies "),e("a",J,[t("Setup Atom"),o(a)]),t(".")])])}const Z=n(c,[["render",K],["__file","index.html.vue"]]);export{Z as default}; diff --git a/assets/index.html.5e5c1bba.js b/assets/index.html.5e5c1bba.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.5e5c1bba.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.5eca0895.js b/assets/index.html.5eca0895.js new file mode 100644 index 0000000000..12d2976dbe --- /dev/null +++ b/assets/index.html.5eca0895.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-08115088","path":"/tag/stars/","title":"stars Tag","lang":"en-US","frontmatter":{"title":"stars Tag","blog":{"type":"category","name":"stars","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{t as data}; diff --git a/assets/index.html.5f67efaf.js b/assets/index.html.5f67efaf.js new file mode 100644 index 0000000000..7a6d639d0a --- /dev/null +++ b/assets/index.html.5f67efaf.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-18ed05d5","path":"/tag/releases/","title":"releases Tag","lang":"en-US","frontmatter":{"title":"releases Tag","blog":{"type":"category","name":"releases","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.61294029.js b/assets/index.html.61294029.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.61294029.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.644bc9d9.js b/assets/index.html.644bc9d9.js new file mode 100644 index 0000000000..fc4b3d8601 --- /dev/null +++ b/assets/index.html.644bc9d9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-f6458faa","path":"/tag/tree-sitter/","title":"tree-sitter Tag","lang":"en-US","frontmatter":{"title":"tree-sitter Tag","blog":{"type":"category","name":"tree-sitter","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.64f1ae66.js b/assets/index.html.64f1ae66.js new file mode 100644 index 0000000000..14b3eec57a --- /dev/null +++ b/assets/index.html.64f1ae66.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-8daa1a0e","path":"/","title":"Home","lang":"en-us","frontmatter":{"sitemap":{"priority":1,"changefreq":"daily"},"lang":"en-us","title":"Home","home":true,"actions":[{"text":"Download \u{1F4E5}","link":"/download.html","type":"primary"},{"text":"Documentation \u{1F4D6}","link":"/docs/"},{"text":"Donate \u{1F381}","link":"/donate.html"}],"heroText":"Pulsar","features":[{"title":"Cross-platform Editing","details":"Pulsar works across operating systems. Use it on OS X, Windows, or Linux."},{"title":"Built-in package manager","details":"Search and install new packages or create your own right from Pulsar."},{"title":"Smart Autocompletion","details":"Pulsar helps you write code faster with a smart and flexible autocomplete."},{"title":"File system browser","details":"Easily browse and open a single file, a whole project, or multiple projects in one window."},{"title":"Multiple panes","details":"Split your Pulsar interface into multiple panes to compare and edit code across files."},{"title":"Find and replace","details":"Find, preview, and replace text as you type in a file or across all your projects."}]},"excerpt":"","headers":[{"level":2,"title":"Notices","slug":"notices","link":"#notices","children":[{"level":3,"title":"Downloads and Releases","slug":"downloads-and-releases","link":"#downloads-and-releases","children":[]},{"level":3,"title":"App Updates","slug":"app-updates","link":"#app-updates","children":[]},{"level":3,"title":"Blog","slug":"blog","link":"#blog","children":[]},{"level":3,"title":"Packages","slug":"packages","link":"#packages","children":[]},{"level":3,"title":"Support and Community","slug":"support-and-community","link":"#support-and-community","children":[]}]}],"git":{"updatedTime":1706814354000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":7},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":7},{"name":"DeeDeeG","email":"DeeDeeG@users.noreply.github.com","commits":5},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":3},{"name":"confused_techie","email":"dev@lhbasics.com","commits":2},{"name":"Joshua MIller","email":"16845458+kaosine@users.noreply.github.com","commits":1},{"name":"Joshua Miller","email":"16845458+kaosine@users.noreply.github.com","commits":1},{"name":"Landar","email":"104514709+LandarXT@users.noreply.github.com","commits":1},{"name":"Meadowsys","email":"blazeykirin@gmail.com","commits":1}]},"readingTime":{"minutes":1.32,"words":396},"filePathRelative":"index.md"}');export{e as data}; diff --git a/assets/index.html.6811c420.js b/assets/index.html.6811c420.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.6811c420.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.6f6341d2.js b/assets/index.html.6f6341d2.js new file mode 100644 index 0000000000..854d6a1850 --- /dev/null +++ b/assets/index.html.6f6341d2.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-cf57a236","path":"/docs/resources/","title":"Resources","lang":"en-US","frontmatter":{"title":"Resources"},"excerpt":"","headers":[{"level":2,"title":"Glossary","slug":"glossary","link":"#glossary","children":[]},{"level":2,"title":"Pulsar API","slug":"pulsar-api","link":"#pulsar-api","children":[]},{"level":2,"title":"Project tooling","slug":"project-tooling","link":"#project-tooling","children":[]}],"git":{"updatedTime":1668306676000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.26,"words":78},"filePathRelative":"docs/resources/index.md"}');export{e as data}; diff --git a/assets/index.html.70b20295.js b/assets/index.html.70b20295.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.70b20295.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.715b0c77.js b/assets/index.html.715b0c77.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.715b0c77.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.719d38d5.js b/assets/index.html.719d38d5.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.719d38d5.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.74bcfb5f.js b/assets/index.html.74bcfb5f.js new file mode 100644 index 0000000000..e45e728afa --- /dev/null +++ b/assets/index.html.74bcfb5f.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3835d9b2","path":"/docs/atom-archive/using-atom/","title":"Chapter 2 : Using Atom","lang":"en-US","frontmatter":{"title":"Chapter 2 : Using Atom","sitemap":{"priority":0.1}},"excerpt":"","headers":[{"level":2,"title":"Using Atom","slug":"using-atom","link":"#using-atom","children":[{"level":3,"title":"Atom Packages","slug":"atom-packages","link":"#atom-packages","children":[]},{"level":3,"title":"Moving in Atom","slug":"moving-in-atom","link":"#moving-in-atom","children":[]},{"level":3,"title":"Atom Selections","slug":"atom-selections","link":"#atom-selections","children":[]},{"level":3,"title":"Editing and Deleting Text","slug":"editing-and-deleting-text","link":"#editing-and-deleting-text","children":[]},{"level":3,"title":"Find and Replace","slug":"find-and-replace","link":"#find-and-replace","children":[]},{"level":3,"title":"Snippets","slug":"snippets","link":"#snippets","children":[]},{"level":3,"title":"Autocomplete","slug":"autocomplete","link":"#autocomplete","children":[]},{"level":3,"title":"Folding","slug":"folding","link":"#folding","children":[]},{"level":3,"title":"Panes","slug":"panes","link":"#panes","children":[]},{"level":3,"title":"Pending Pane Items","slug":"pending-pane-items","link":"#pending-pane-items","children":[]},{"level":3,"title":"Grammar","slug":"grammar","link":"#grammar","children":[]},{"level":3,"title":"Version Control in Atom","slug":"version-control-in-atom","link":"#version-control-in-atom","children":[]},{"level":3,"title":"GitHub package","slug":"github-package","link":"#github-package","children":[]},{"level":3,"title":"Writing in Atom","slug":"writing-in-atom","link":"#writing-in-atom","children":[]},{"level":3,"title":"Basic Customization","slug":"basic-customization","link":"#basic-customization","children":[]},{"level":3,"title":"Summary","slug":"summary","link":"#summary","children":[]}]}],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":3},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":0.86,"words":259},"filePathRelative":"docs/atom-archive/using-atom/index.md"}');export{e as data}; diff --git a/assets/index.html.758e28c3.js b/assets/index.html.758e28c3.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.758e28c3.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.782819d8.js b/assets/index.html.782819d8.js new file mode 100644 index 0000000000..42e095e1c8 --- /dev/null +++ b/assets/index.html.782819d8.js @@ -0,0 +1,65 @@ +import{_ as o,o as i,c as r,a,b as e,d as n,f as t,r as p}from"./app.0e1565ce.js";const c={},l=t('STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
This appendix covers the the Atom server-side APIs that various parts of Atom consume.
WARNING
Warning: These APIs should be considered pre-release and are subject to change.
All requests that take parameters require application/json
.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to downloads
asc
or desc
. Defaults to desc
. stars
can only be ordered desc
Returns a list of all packages in the following format:
[
+ {
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ }
+ },
+ ...
+ ]
+
Results are paginated 30 at a time, and links to the next and last pages are provided in the Link
header:
Link: <https://www.atom.io/api/packages?page=1>; rel="self",
+ <https://www.atom.io/api/packages?page=41>; rel="last",
+ <https://www.atom.io/api/packages?page=2>; rel="next"
+
By default, results are sorted by download count, descending.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to the relevance of the search query.asc
or desc
. Defaults to desc
.Returns results in the same format as listing packages.
Returns package details and versions for a single package
Parameters:
`,22),x=a("strong",null,"engine",-1),w={href:"http://semver.org",target:"_blank",rel:"noopener noreferrer"},A=t(`Returns:
{
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ },
+ "versions": [
+ (see single version output below)
+ ...,
+ ]
+ }
+
Create a new package; requires authentication.
The name and version will be fetched from the package.json
file in the specified repository. The authenticating user must have access to the indicated repository.
Parameters:
Returns:
Delete a package; requires authentication.
Returns:
Packages are renamed by publishing a new version with the name changed in package.json
. See Creating a new package version for details.
Requests made to the previous name will forward to the new name.
Returns package.json
with dist
key added for e.g. tarball download:
{
+ "bugs": {
+ "url": "https://github.com/thedaniel/test-package/issues"
+ },
+ "dependencies": {
+ "async": "~0.2.6",
+ "pegjs": "~0.7.0",
+ "season": "~0.13.0"
+ },
+ "description": "Expand snippets matching the current prefix with \`tab\`.",
+ "dist": {
+ "tarball": "https://codeload.github.com/..."
+ },
+ "engines": {
+ "atom": "*"
+ },
+ "main": "./lib/snippets",
+ "name": "thedaniel-test-package",
+ "publishConfig": {
+ "registry": "https://..."
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package.git"
+ },
+ "version": "0.6.0"
+}
+
Creates a new package version from a git tag; requires authentication. If rename
is not true
, the name
field in package.json
must match the current package name.
Parameters:
version
key in the package.json
file at that ref. The authenticating user must have access to the package repository.Returns:
Deletes a package version; requires authentication.
Note that a version cannot be republished with a different tag if it is deleted. If you need to delete the latest version of a package for example for security reasons, you'll need to increment the version when republishing.
Returns 204 No Content
List a user's starred packages.
Return value is similar to GET /api/packages
List the authenticated user's starred packages; requires authentication.
Return value is similar to GET /api/packages
Star a package; requires authentication.
Returns a package.
Unstar a package; requires authentication.
Returns 204 No Content.
List the users that have starred a package.
Returns a list of user objects:
[{ "login": "aperson" }, { "login": "anotherperson" }]
+
WARNING
Warning: This API should be considered pre-release and is subject to change.
Returns:
{
+ "name": "0.96.0",
+ "notes": "[HTML release notes]",
+ "pub_date": "2014-05-19T15:52:06.000Z",
+ "url": "https://www.atom.io/api/updates/download"
+}
+
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
The collection of Frequently Asked Questions about Atom.
Atom's Safe Mode, which can be activated by completely exiting all instances of Atom and launching it again using the command atom --safe
from the command line, does the following:
~/.atom/packages
or ~/.atom/dev/packages
init.coffee
The intent of Safe Mode is to determine if a problem is being caused by a community package or is caused by built-in functionality of Atom. Disabling the init script was added because people tend to use the init script as a mini-package of sorts by adding code, commands and other functionality that would normally be in a package.
',4),Z={href:"https://pulsar-edit.dev/docs/atom-archive/hacking-atom/#debugging",target:"_blank",rel:"noopener noreferrer"},ee=e("h3",{id:"i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it","aria-hidden":"true"},"#"),t(" I have a question about a specific Atom community package. Where is the best place to ask it?")],-1),te=e("p",null,"The best place to get a question answered quickly is probably the Issues list for that specific package. You can find the Issues list for a package by going to that package's page on https://atom.io and clicking the Bugs button:",-1),oe=e("p",null,[e("img",{src:c,alt:"Bugs button link"})],-1),ae={href:"https://github.com/atom/atom/discussions",target:"_blank",rel:"noopener noreferrer"},ne=e("h3",{id:"i-m-using-an-international-keyboard-and-keys-that-use-altgr-or-ctrl-alt-aren-t-working",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#i-m-using-an-international-keyboard-and-keys-that-use-altgr-or-ctrl-alt-aren-t-working","aria-hidden":"true"},"#"),t(" I\u2019m using an international keyboard and keys that use AltGr or Ctrl+Alt aren\u2019t working")],-1),ie={href:"http://blog.atom.io/2016/10/17/the-wonderful-world-of-keyboards.html",target:"_blank",rel:"noopener noreferrer"},se=e("h3",{id:"i-m-having-a-problem-with-julia-what-do-i-do",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#i-m-having-a-problem-with-julia-what-do-i-do","aria-hidden":"true"},"#"),t(" I\u2019m having a problem with Julia! What do I do?")],-1),re={href:"http://junolab.org/",target:"_blank",rel:"noopener noreferrer"},le={href:"https://discourse.julialang.org/c/tools/juno",target:"_blank",rel:"noopener noreferrer"},ce=n(`This means that there is a proxy between you and our servers where someone (typically your employer) has installed a "self-signed" security certificate in the proxy. A self-signed certificate is one that isn't trusted by anyone but the person who created the certificate. Most security certificates are backed by known, trusted and certified companies. So Atom is warning you that your connection to our servers can be snooped and even hacked by whoever created the self-signed certificate. Since it is self-signed, Atom has no way of knowing who that is.
If you decide that unsecured connections to our servers is acceptable to you, you can use the following instructions.
DANGER
\u{1F6A8} Danger: If you decide that unsecured connections to our servers is acceptable to you, you can use the following command:
apm config set strict-ssl false
+
Atom includes a feature called "custom file types" which you can use by adding some entries into your config.cson
that look like this:
core:
+ customFileTypes:
+ 'source.ruby': [
+ 'Cheffile'
+ 'this-is-also-ruby'
+ ]
+ 'source.cpp': [
+ 'h'
+ ]
+
You can make the Welcome screen stop showing up by unchecking this box in the welcome screen itself:
There are a couple different approaches, for example:
',5),fe={href:"https://atom.io/packages/browser-plus",target:"_blank",rel:"noopener noreferrer"},be={href:"https://atom.io/packages/livereload",target:"_blank",rel:"noopener noreferrer"},ye={href:"https://atom.io/packages",target:"_blank",rel:"noopener noreferrer"},we=e("h3",{id:"how-do-i-accept-input-from-my-program-or-script-when-using-the-script-package",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#how-do-i-accept-input-from-my-program-or-script-when-using-the-script-package","aria-hidden":"true"},"#"),t(" How do I accept input from my program or script when using the script package?")],-1),ve={href:"https://atom.io/packages/script",target:"_blank",rel:"noopener noreferrer"},ke={href:"https://atom.io/packages/search?q=terminal",target:"_blank",rel:"noopener noreferrer"},_e={href:"https://github.com/rgbkrk/atom-script/issues/743",target:"_blank",rel:"noopener noreferrer"},xe=n('Atom shows there is a new version available but the version fails to install. You might have an error message showing a permissions error for example:
or it will say downloading but forever loops without restarting or updating.
You need to fix one or more of the following directories:
/Applications/Atom.app/
~/Library/Caches/com.github.atom.ShipIt
~/Library/Application Support/com.github.atom.ShipIt
Do the following:
whoami
And then execute these steps for each directory listed above in order:
stat -f "%Su" [directory]
root
root
then execute: sudo chown -R $(whoami) [directory]
Once you've done the above for both directories, start Atom normally and attempt to update \u{1F44D}
The best way to tweak the syntax is to wrap your syntax style rules with atom-text-editor
and then prepend every scope with syntax--
. If you want your comments to be blue, for example, you would do the following:
atom-text-editor {
+ .syntax--comment {
+ color: blue;
+ }
+}
+
To uninstall Atom on macOS, run the following commands from the command line:
rm -rf ~/.atom
+rm -rf /usr/local/bin/atom
+rm -rf /usr/local/bin/apm
+rm -rf /Applications/Atom.app
+rm -rf ~/Library/Preferences/com.github.atom.plist
+rm -rf "~/Library/Application Support/com.github.atom.ShipIt"
+rm -rf "~/Library/Application Support/Atom"
+rm -rf "~/Library/Saved Application State/com.github.atom.savedState"
+rm -rf ~/Library/Caches/com.github.atom
+rm -rf ~/Library/Caches/Atom
+
If this change is something that you dislike, there are a couple workarounds that the community has identified.
defaults write -g CGFontRenderingFontSmoothingDisabled -bool NO
This appears to re-enable the old "LCD font smoothing" option that was removed in Mojave. It is important to note that this is an OS-wide change.
atom-text-editor {
+ font-weight: bold;
+}
+
This has the benefit of being a change local to Atom only if the rest of the OS looks fine to you.
With macOS 10.14 Mojave, Apple introduced new privacy protections similar to the existing protections found in iOS. Whenever an application attempts to access the files inside certain newly-protected directories, macOS asks the user whether they want to allow the application to access the content in those directories. These new privacy protections apply to the directories that contain your calendars, contacts, photos, mail, messages, and Time Machine backups.
Applications trigger these new macOS prompts when attempting to access these directories in any way. Simply attempting to list the files in one of these directories is enough to trigger these prompts. These protections even apply to Apple's own applications. For example, if you open Terminal.app
and try to list the files in ~/Library/Calendars
, macOS shows a prompt saying, '"Terminal" would like access to your calendar.'
In addition to containing the files you're intending to edit inside Atom, your home directory also contains your files that have new OS-level protections in Mojave:
~/Library/Calendars
)~/Library/Application\\ Support/AddressBook
~/Library/Mail
)~/Pictures/Photos\\ Library.photoslibrary
)Before letting Atom read these files, Mojave is understandably asking whether you want Atom to be able to access this personal data.
Most people don't use Atom to view or edit their calendar files, contact files, photo library, etc. If you don't intend to use Atom to view/edit these files, then Atom doesn't need access to them. If you see a prompt from macOS saying that Atom would like to access these items, simply click Don't Allow.
To Atom, these items are just files on disk. Atom treats them exactly like any other file you would view in Atom. Therefore, if you allow Atom to access these items, you'll be able to use Atom to browse the directories that contain these items, and you'll be able to view the files in those directories. That's it. Nothing more.
Fortunately, macOS will only prompt you once for each type of personal data. In other words, you might see a prompt asking you whether Atom can access your calendar, and you might see a prompt asking you whether Atom can access your contacts, but once you make those decisions, you won't see those prompts again.
At any time, you can change your choices via System Preferences. Inside System Preferences, go to Security and Privacy
, click the Privacy
tab, and then click on Calendars
to manage which apps can access your Calendars
. The same goes for Contacts
, Photos
, etc.:
Many people understandably expect their text editor to be able to open any file on disk. And that's exactly how things worked prior to macOS Mojave. If you would like to restore that behavior, you can proactively instruct macOS to allow you to access all files with Atom. To do so:
Applications
folder in the FinderSecurity and Privacy
icon, click the Privacy
tab, and then click on Full Disk Access
in the left-hand sidebarFull Disk Access
as shown belowFor more details about soft wrap, see: https://flight-manual.atom.io/getting-started/sections/atom-basics/#soft-wrap.
If you're running Windows or Linux and you don't see the menu bar, it may have been accidentally toggled it off. You can bring it back from the Command Palette with Window: Toggle Menu Bar
or by pressing Alt.
You can disable hiding the menu bar with Alt by unchecking Settings > Core > Auto Hide Menu Bar
.
To use a newline in the result of find and replace, enable the Use Regex
option and use "\\n" in your replacement text. For example, given this text:
hello, world, goodbye
+
If you'd like to replace the ", " with a newline so you end up with this text:
hello
+world
+goodbye
+
In the find and replace settings, enable Use Regex
, enter ", " as the find text, and enter "\\n" as the replace text:
Then click Find All
and finally, click Replace All
.
That's the wrap guide. It is a visual indicator of when your lines of code are getting too long. It defaults to the column that your Preferred Line Length is set to.
If you want to turn it off, you can disable the wrap-guide package in the Settings View.
',34);function Ne(Ve,Je){const a=i("ExternalLinkIcon"),s=i("RouterLink");return g(),f("div",null,[w,e("p",null,[t("Yes, "),e("a",v,[t("Atom is licensed under the MIT license."),o(a)])]),k,e("p",null,[t("Since the 6"),_,t(" of May, 2014, "),e("a",x,[t("Atom has been available for download free of charge for everyone"),o(a)]),t(". This includes business and enterprise use.")]),A,I,e("p",null,[t("If you would like to build from source on Windows, Linux, or OS X, see the "),e("a",S,[t("Atom README"),o(a)]),t(" for more information.")]),T,e("p",null,[t("You can contribute by "),e("a",q,[t("creating a package"),o(a)]),t(" that adds something awesome to Atom!")]),e("p",null,[t("Also, if you\u2019d like to contribute to the core editor, one of the bundled packages, or one of the libraries that power Atom, just go to "),e("a",O,[t("github.com/atom"),o(a)]),t(".")]),e("p",null,[t("You should also read the "),e("a",W,[t("contributing guide"),o(a)]),t(" before getting started.")]),C,L,P,e("p",null,[t("The Atom team has no plans to make a cloud- or server-based version of Atom. For discussion of the idea, see the "),e("a",M,[t("Atom message board"),o(a)]),t(".")]),E,j,e("p",null,[t("An editor is simply that, a tool that is designed to edit text. Typically they are optimized for programming languages though many programmer's text editors are branching out and adding features for non-programming text like "),e("a",D,[t("Markdown"),o(a)]),t(" or "),e("a",R,[t("Org Mode"),o(a)]),t(". The key here is that text editors are designed to work with whatever language or framework you choose.")]),F,Y,z,H,B,N,e("p",null,[t("You can find more information on "),e("a",V,[t("subpixel rendering on Wikipedia"),o(a)]),t(".")]),J,e("p",null,[t("Atom ships with the "),e("a",$,[U,t(" package"),o(a)]),t(", which by default strips trailing whitespace from lines in your file, and inserts a final trailing newline to indicate end-of-file "),e("a",X,[t("as per the POSIX standard"),o(a)]),t(".")]),G,Q,e("p",null,[t("Take a look at "),o(s,{to:"/using-atom/sections/editing-and-deleting-text/#whitespace"},{default:b(()=>[t("the Whitespace section")]),_:1}),t(" for more information.")]),K,e("p",null,[t("For more information on Safe Mode, check the "),e("a",Z,[t("debugging section"),o(a)]),t(".")]),ee,te,oe,e("p",null,[t("And you can always ask Atom-related questions in the "),e("a",ae,[t("official Atom message board"),o(a)]),t(". Someone here may know the answer! It's just with over 3,500 packages (as of early February 2016), the forum members may not know all answers for all packages \u{1F600}")]),ne,e("p",null,[t("As of Atom v1.12, a fix is available for this. See "),e("a",ie,[t('the blog post "The Wonderful World of Keyboards"'),o(a)]),t(" for more information.")]),se,e("p",null,[e("a",re,[t("Juno"),o(a)]),t(" is a development environment built on top of Atom but has enough separate customizations that they have their own "),e("a",le,[t("message board"),o(a)]),t(". You will probably have better luck asking your question there.")]),ce,e("p",null,[e("a",he,[t("PlatformIO"),o(a)]),t(" is a development environment built on top of Atom but has enough separate customizations that they have their own "),e("a",de,[t("message board"),o(a)]),t(". If your question has to do with PlatformIO specifically, you may have better luck getting your answer there.")]),ue,e("p",null,[t("The key (for example "),pe,t(" in the above snippet) is the language's "),e("a",me,[t("scope name"),o(a)]),t(". The value is an array of file extensions, without the period, to match to that scope name.")]),ge,e("ul",null,[e("li",null,[e("a",fe,[t("browser-plus"),o(a)]),t(" gives a reasonably full browser implementation within Atom")]),e("li",null,[e("a",be,[t("livereload"),o(a)]),t(" gives you a preview in any browser, but requires you to save the file first.")])]),e("p",null,[t("Other packages may be available now, you can search for Atom packages on the "),e("a",ye,[t("packages site"),o(a)]),t(".")]),we,e("p",null,[t("The "),e("a",ve,[t("script package"),o(a)]),t(" doesn't support accepting input from the user in the scripts it runs. The option with the best chance of success is to run the script or program from the terminal that comes with your operating system. If that isn't something you want to do, you could try one of the many "),e("a",ke,[t("terminal packages"),o(a)]),t(" that are available.")]),e("p",null,[t("See "),e("a",_e,[t("rgbkrk/atom-script#743"),o(a)]),t(" for details.")]),xe,e("p",null,[t("Atom doesn't have built-in support for building any type of code nor does it have built-in support for executing any kind of code other than JavaScript. Atom has a JavaScript interactive command-line (also known as a "),e("a",Ae,[t("REPL"),o(a)]),t(") available through the Developer Tools. You can access the JavaScript REPL by using the following steps:")]),Ie,Se,Te,e("ul",null,[e("li",null,[e("a",qe,[t("build"),o(a)])]),e("li",null,[e("a",Oe,[t("script"),o(a)])])]),We,Ce,e("ul",null,[e("li",null,[e("a",Le,[t("Python"),o(a)])])]),Pe,e("p",null,[t("In "),e("a",Me,[t("macOS Mojave v10.14.x"),o(a)]),t(', Apple disabled subpixel antialiasing on all monitors by default. Previous to Mojave, subpixel antialiasing was disabled only on Retina displays or on all displays if the "LCD font smoothing" option was disabled in System Preferences. With this change in Mojave, some users have reported that '),e("a",Ee,[t('their fonts in Atom appear "thinner" or "dimmer" than they did previously.'),o(a)]),t(" It can look better or worse depending on your font and theme selections, but in all cases this is completely a side-effect of the change that Apple made to their font rendering and is outside Atom's and Electron's control.")]),je,e("p",null,[t("Add the following to "),e("a",De,[t("your stylesheet"),o(a)]),t(":")]),Re,e("p",null,[t("Atom doesn't need access to these items, but you might unintentionally cause Atom to try to access these items. This commonly occurs when you open your home directory ("),Fe,t(") inside Atom and run a command that examines all files and directories beneath your home directory. For example, when you open the "),e("a",Ye,[t("fuzzy-finder"),o(a)]),t(", it indexes the currently-open directory so that it can show you the available files:")]),ze,e("p",null,[t("Similarly, using "),e("a",He,[t("find-and-replace"),o(a)]),t(" across the entire home directory will cause Atom to scan all files under your home directory.")]),Be])}const tt=m(y,[["render",Ne],["__file","index.html.vue"]]);export{tt as default}; diff --git a/assets/index.html.7ceb711f.js b/assets/index.html.7ceb711f.js new file mode 100644 index 0000000000..47a448a17e --- /dev/null +++ b/assets/index.html.7ceb711f.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0b77a069","path":"/tag/backend/","title":"backend Tag","lang":"en-US","frontmatter":{"title":"backend Tag","blog":{"type":"category","name":"backend","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.817cae06.js b/assets/index.html.817cae06.js new file mode 100644 index 0000000000..2b93019bb2 --- /dev/null +++ b/assets/index.html.817cae06.js @@ -0,0 +1,70 @@ +import{_ as h,a as u,b as p}from"./windows-downloads.e9b59e10.js";import{_ as g}from"./windows-system-settings.141561e2.js";import{_ as f,a as b,b as y,c as w,d as v,e as _,f as k,g as x}from"./finder.04f876a5.js";import{_ as A}from"./platform-selector.697b4dd8.js";import{_ as S,o as T,c as C,d as o,w as a,a as e,b as t,f as r,r as m}from"./app.0e1565ce.js";const I={},P=r('STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, you use this at your own risk. Current Pulsar documentation for this section is found at the documentation home.
This chapter is about getting started with Atom.
There are a lot of text editors out there; why should you spend your time learning about and using Atom?
Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Atom to build Atom, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Atom is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local subprocesses was a non-starter.
For this reason, we didn't build Atom as a traditional web application. Instead, Atom is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Atom window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Atom, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Atom on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Atom as a perfect complement to GitHub's primary mission of building better software by working together. Atom is a long-term investment, and GitHub will continue to support its development with a dedicated team going forward. But we also know that we can't achieve our vision for Atom alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
The entire Atom editor is free and open source and is available under the https://github.com/atom organization.
To get started with Atom, we'll need to get it on your system. This section will go over installing Atom on your system, as well as the basics of how to build it from source.
Installing Atom should be fairly simple. Generally, you can go to https://atom.io and you should see a download button as shown here:
',28),O=e("p",null,[e("img",{src:h,alt:"Download buttons on https://atom.io",title:"Download buttons on https://atom.io"})],-1),z=e("p",null,[e("img",{src:u,alt:"Download buttons on https://atom.io",title:"Download buttons on https://atom.io"})],-1),F=e("p",null,[e("img",{src:p,alt:"Download buttons on https://atom.io",title:"Download buttons on https://atom.io"})],-1),$=e("p",null,"The button or buttons should be specific to your platform and the download package should be easily installable. However, let's go over them here in a bit of detail.",-1),Y=e("h4",{id:"installing-atom-on-linux",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#installing-atom-on-linux","aria-hidden":"true"},"#"),t(" Installing Atom on Linux")],-1),E=e("p",null,"You can install Atom on Linux using your distribution's package manager by configuring it to use one of our official package repositories. This will also enable you to update Atom when new releases are published.",-1),N=e("h5",{id:"debian-and-ubuntu-deb-apt",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#debian-and-ubuntu-deb-apt","aria-hidden":"true"},"#"),t(" Debian and Ubuntu (deb/apt)")],-1),W=e("p",null,"To install Atom on Debian, Ubuntu, or related distributions, add our official package repository to your system by running the following commands:",-1),U=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ wget -qO - https://packagecloud.io/AtomEditor/atom/gpgkey | sudo apt- +key add - +$ sudo sh -c 'echo "deb [arch=amd64] https://packagecloud.io/AtomEditor/atom/any/ any main" > /etc/apt/sources.list.d/atom.list' +$ sudo apt-get update +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),q=e("p",null,[t("You can now install Atom using "),e("code",null,"apt-get"),t(" (or "),e("code",null,"apt"),t(" on Ubuntu):")],-1),V=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`# Install Atom +$ sudo apt-get install atom + +# Install Atom Beta +$ sudo apt-get install atom-beta +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),j={href:"https://atom.io/download/deb",target:"_blank",rel:"noopener noreferrer"},M=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`# Install Atom +$ sudo apt install ./atom-amd64.deb +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),L=e("h5",{id:"red-hat-and-centos-yum-or-fedora-dnf",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#red-hat-and-centos-yum-or-fedora-dnf","aria-hidden":"true"},"#"),t(" Red Hat and CentOS (YUM), or Fedora (DNF)")],-1),B=e("p",null,"To install Atom on CentOS, Oracle Linux, Red Hat Enterprise Linux, Scientific Linux, Fedora, or related distributions that use the YUM or DNF package managers, add our official package repository to your system by running the following commands:",-1),D=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo rpm --import https://packagecloud.io/AtomEditor/atom/gpgkey +$ sudo sh -c 'echo -e "[Atom]\\nname=Atom Editor\\nbaseurl= +https://packagecloud.io/AtomEditor/atom/el/7/ +\\$basearch\\nenabled=1\\ngpgcheck=0\\nrepo_gpgcheck=1\\ngpgkey= +https://packagecloud.io/AtomEditor/atom/gpgkey" > /etc/yum.repos.d/ +atom.repo' +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),H=e("p",null,[t("You can now install Atom using "),e("code",null,"dnf"),t(" (or "),e("code",null,"yum"),t(" depending on your distribution):")],-1),G=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`# Install Atom +$ sudo dnf install atom + +# Install Atom Beta +$ sudo dnf install atom-beta +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),R={href:"https://atom.io/download/rpm",target:"_blank",rel:"noopener noreferrer"},J=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`# On YUM-based distributions +$ sudo yum install -y atom.x86_64.rpm + +# On DNF-based distributions +$ sudo dnf install -y atom.x86_64.rpm +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),K=e("h5",{id:"suse-zypp",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#suse-zypp","aria-hidden":"true"},"#"),t(" SUSE (zypp)")],-1),X=e("p",null,"To install Atom on openSUSE or other distributions that use the Zypp package manager, add our official package repository to your system by running the following commands:",-1),Z=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo sh -c 'echo -e "[Atom]\\nname=Atom Editor\\nbaseurl= +https://packagecloud.io/AtomEditor/atom/el/7/\\$basearch\\nenabled=1\\ntype=rpm- +md\\ngpgcheck=0\\nrepo_gpgcheck=1\\ +ngpgkey=https://packagecloud.io/AtomEditor/atom/gpgkey" > +/etc/zypp/repos.d/atom.repo' +$ sudo zypper --gpg-auto-import-keys refresh +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),Q=e("p",null,[t("You can now install Atom using "),e("code",null,"zypper"),t(":")],-1),ee=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`# Install Atom +$ sudo zypper install atom + +# Install Atom Beta +$ sudo zypper install atom-beta +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),te={href:"https://atom.io/download/rpm",target:"_blank",rel:"noopener noreferrer"},oe=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ sudo zypper in -y atom.x86_64.rpm +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"})])],-1),ae=e("h4",{id:"installing-atom-on-mac",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#installing-atom-on-mac","aria-hidden":"true"},"#"),t(" Installing Atom on Mac")],-1),ne={href:"https://github.com/atom/atom/releases/latest",target:"_blank",rel:"noopener noreferrer"},ie=e("code",null,"atom-mac.zip",-1),se=e("code",null,"Atom",-1),le=e("p",null,[t("When you first open Atom, it will try to install the "),e("code",null,"atom"),t(" and "),e("code",null,"apm"),t(" commands for use in the terminal. In some cases, Atom might not be able to install these commands because it needs an administrator password. To check if Atom was able to install the "),e("code",null,"atom"),t(" command, for example, open a terminal window and type "),e("code",null,"which atom"),t(". If the "),e("code",null,"atom"),t(" command has been installed, you'll see something like this:")],-1),re=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ which atom +> /usr/local/bin/atom +$ +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),de=e("p",null,[t("If the "),e("code",null,"atom"),t(" command wasn't installed, the "),e("code",null,"which"),t(" command won't return anything:")],-1),ce=e("div",{class:"language-command-line ext-command-line line-numbers-mode"},[e("pre",{class:"language-command-line"},[e("code",null,`$ which atom +$ +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),me=e("p",null,[t("To install the "),e("code",null,"atom"),t(" and "),e("code",null,"apm"),t(' commands, run "Window: Install Shell Commands" from the '),e("a",{href:"/getting-started/sections/atom-basics#command-palette"},"Command Palette"),t(", which will prompt you for an administrator password.")],-1),he=e("h4",{id:"installing-atom-on-windows",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#installing-atom-on-windows","aria-hidden":"true"},"#"),t(" Installing Atom on Windows")],-1),ue={href:"https://github.com/atom/atom/releases/latest",target:"_blank",rel:"noopener noreferrer"},pe=e("code",null,"AtomSetup.exe",-1),ge=e("code",null,"AtomSetup-x64.exe",-1),fe=e("code",null,"atom",-1),be=e("code",null,"apm",-1),ye=e("code",null,"PATH",-1),we=e("p",null,[e("img",{src:g,alt:"Atom on Windows"})],-1),ve=e("p",null,[t("The context menu "),e("code",null,"Open with Atom"),t(" in File Explorer, and the option to make Atom available for file association using "),e("code",null,"Open with..."),t(", is controlled by the System Settings panel as seen above.")],-1),_e=e("p",null,[t("With Atom open, click on "),e("code",null,"File > Settings"),t(", and then the "),e("code",null,"System"),t(" tab on the left. Check the boxes next to "),e("code",null,"Show in file context menus"),t(", as well as "),e("code",null,"Show in folder context menus"),t(". And you\u2019re all set.")],-1),ke=e("h4",{id:"updating-atom",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#updating-atom","aria-hidden":"true"},"#"),t(" Updating Atom")],-1),xe=e("p",null,"You should consider updating Atom periodically for the latest improvements to the software. Additionally, When Atom receives hotfixes for security vulnerabilities you will want to update your version of Atom as soon as possible.",-1),Ae=e("code",null,".rpm",-1),Se=e("code",null,".deb",-1),Te={href:"https://flight-manual.atom.io/getting-started/sections/installing-atom/#installing-atom-on-linux",target:"_blank",rel:"noopener noreferrer"},Ce={href:"https://flight-manual.atom.io/getting-started/sections/atom-basics/#settings-and-preferences",target:"_blank",rel:"noopener noreferrer"},Ie=e("p",null,"To perform a manual update:",-1),Pe=e("li",null,[t("Click on the "),e("code",null,"Atom > Check for Update"),t(" menu item in the menu bar.")],-1),Oe=e("code",null,"Application: About",-1),ze={href:"https://flight-manual.atom.io/getting-started/sections/atom-basics/#command-palette",target:"_blank",rel:"noopener noreferrer"},Fe=e("code",null,"Check now",-1),$e=e("p",null,"Atom will begin to update if an update is available.",-1),Ye={href:"https://flight-manual.atom.io/getting-started/sections/atom-basics/#settings-and-preferences",target:"_blank",rel:"noopener noreferrer"},Ee=e("p",null,"To perform a manual update:",-1),Ne=e("li",null,[t("Click on the "),e("code",null,"Help > Check for Update"),t(" menu item in the menu bar.")],-1),We=e("code",null,"Application: About",-1),Ue={href:"https://flight-manual.atom.io/getting-started/sections/atom-basics/#command-palette",target:"_blank",rel:"noopener noreferrer"},qe=e("code",null,"Check now",-1),Ve=e("p",null,"Atom will begin to update if an update is available.",-1),je=e("h4",{id:"portable-mode",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#portable-mode","aria-hidden":"true"},"#"),t(" Portable Mode")],-1),Me=e("p",null,[t("Atom stores configuration and state in a "),e("code",null,".atom"),t(" directory usually located in your home directory "),e("span",{class:"platform-windows"},[t("("),e("code",null,"%userprofile%"),t(" on Windows)")]),t(". You can however run Atom in portable mode where both the app and the configuration are stored together such as on a removable storage device.")],-1),Le={href:"https://github.com/atom/atom/releases/latest",target:"_blank",rel:"noopener noreferrer"},Be=e("p",null,[t("Then create a "),e("code",null,".atom"),t(" directory alongside the directory that contains the Atom binary, for example:")],-1),De=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`/media/myusb/atom-1.14/atom +/media/myusb/.atom +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),He=e("p",null,[t("Then create a "),e("code",null,".atom"),t(" directory alongside the Atom.app application, for example:")],-1),Ge=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`/MyUSB/Atom.app +/MyUSB/.atom +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),Re=e("p",null,[t("Then create a "),e("code",null,".atom"),t(" directory alongside the directory that contains atom.exe, for example:")],-1),Je=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`e:\\atom-1.14\\atom.exe +e:\\.atom +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),Ke=r('.atom
directory must be writeable.atom
directory to your portable device.atom
directory - just create a subdirectory called electronUserData
inside .atom
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ apm config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure apm
to use it by running:
$ apm config set https-proxy <em>YOUR_PROXY_ADDRESS</em>
+
You can run apm config get https-proxy
to verify it has been set correctly.
Now that Atom is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Atom for the first time, you should get a screen that looks like this:
This is the Atom welcome screen and gives you a pretty good starting point for how to get started with the editor.
In that welcome screen, we are introduced to probably the most important command in Atom, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like Cmd+Shift+PCtrl+Shift+P to demonstrate how to run a command. These are the default keybindings for the platform that we detected you running.
If you want to see a different platform than the one we detected, you may choose a different one by using the platform selector near the top of the page:
If the Platform Selector is not present, then the current page doesn't have any platform-specific content.
If you have customized your Atom keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Atom. Instead of clicking around all the application menus to look for something, you can press Cmd+Shift+PCtrl+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut keystrokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Atom has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Atom Packages.
To open the Settings View, you can: ::: tabs Settings View
:::
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Atom. Atom ships with 4 different UI themes, dark and light variants of the Atom and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
There are also dozens of themes on https://atom.io that you can choose from if you want something different. We will cover customizing a theme in Style Tweaks and creating your own theme in Creating a Theme.
You can use the Settings View to specify your whitespace and wrapping preferences.
Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the Tab key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.
The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.
',13),et=r('Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Atom. You can do it by choosing File > Open from the menu bar or by pressing Cmd+O Ctrl+O to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
',6),tt=e("code",null,"atom",-1),ot={class:"platform-mac"},at=e("code",null,"atom",-1),nt=e("code",null,"apm",-1),it={class:"platform-windows platform-linux"},st=e("code",null,"atom",-1),lt=e("code",null,"apm",-1),rt=r(`You can run the atom
command with one or more file paths to open up those files in Atom.
$ atom --help
+> Atom Editor v1.8.0
+
+> Usage: atom [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Atom window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off atom [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+content/getting-started/sections/atom-basics.md:84:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ atom content/getting-started/sections/atom-basics.md:84
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+content/getting-started/sections/atom-basics.md:150:722
+$ atom content/getting-started/sections/atom-basics.md:150:722
+
To save a file you can choose File > Save from the menu bar or Cmd+SCtrl+S to save the file. If you choose File > Save As or press Cmd+Shift+SCtrl+Shift+S then you can save the current content in your editor under a different file name. Finally, you can choose File > Save All or press Alt+Cmd+S to save all the open files in Atom.
Atom doesn't just work with single files though; you will most likely spend most of your time working on projects with multiple files. To open a directory, choose the menu item File > OpenFile > Open Folder and select a directory from the dialog. You can also add more than one directory to your current Atom window, by choosing File > Add Project Folder from the menu bar or pressing Cmd+Shift+OCtrl+Shift+A.
You can open any number of directories from the command line by passing their paths to the atom
command line tool. For example, you could run the command atom ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Atom with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
You can also hide and show it with Cmd+\\Ctrl+\\ or the tree-view: toggle
command from the Command Palette, and Ctrl+0 Alt+\\ will focus it. When the Tree view has focus you can press A, M, or Delete to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in FinderWindows Exploreryour native filesystem or copying the file path to the clipboard.
Note
Atom Packages
Like many parts of Atom, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Atom by default. Packages that are bundled with Atom are referred to as Core packages. Ones that aren't bundled with Atom are referred to as Community packages.
You can find the source code to the Tree View on GitHub at https://github.com/atom/tree-view.
This is one of the interesting things about Atom. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.
Once you have a project open in Atom, you can easily find and open any file within that project.
If you press Cmd+TCtrl+T or Cmd+PCtrl+P, the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.
You can also search through only the files currently opened (rather than every file in your project) with Cmd+BCtrl+B. This searches through your "buffers" or open files. You can also limit this fuzzy search with Cmd+Shift+BCtrl+Shift+B, which searches only through the files which are new or have been modified since your last Git commit.
',14),mt=e("code",null,"core.ignoredNames",-1),ht=e("code",null,"fuzzy-finder.ignoredNames",-1),ut=e("code",null,"core.excludeVCSIgnoredPaths",-1),pt={href:"https://git-scm.com/docs/gitignore",target:"_blank",rel:"noopener noreferrer"},gt=e("code",null,".gitignore",-1),ft=e("code",null,"core.ignoredNames",-1),bt=e("code",null,"fuzzy-finder.ignoredNames",-1),yt={href:"https://github.com/isaacs/minimatch",target:"_blank",rel:"noopener noreferrer"},wt=r('Tips
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
You should now have a basic understanding of what Atom is and what you want to do with it. You should also have it installed on your system and be able to use it for the most basic text editing operations.
Now you're ready to start digging into the fun stuff.
',4);function vt(_t,kt){const c=m("Tabs"),n=m("ExternalLinkIcon"),d=m("RouterLink");return T(),C("div",null,[P,o(c,{id:"89",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"installing-atom"},{tab0:a(({title:i,value:s,isActive:l})=>[O]),tab1:a(({title:i,value:s,isActive:l})=>[z]),tab2:a(({title:i,value:s,isActive:l})=>[F]),_:1}),$,o(c,{id:"109",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"installing-atom"},{tab0:a(({title:i,value:s,isActive:l})=>[Y,E,N,W,U,q,V,e("p",null,[t("Alternatively, you can download the "),e("a",j,[t("Atom .deb package"),o(n)]),t(" and install it directly:")]),M,L,B,D,H,G,e("p",null,[t("Alternatively, you can download the "),e("a",R,[t("Atom .rpm package"),o(n)]),t(" and install it directly:")]),J,K,X,Z,Q,ee,e("p",null,[t("Alternatively, you can download the "),e("a",te,[t("Atom .rpm package"),o(n)]),t(" and install it directly:")]),oe]),tab1:a(({title:i,value:s,isActive:l})=>[ae,e("p",null,[t("Atom follows the standard Mac zip installation process. You can either press the download button from the https://atom.io site or you can go to the "),e("a",ne,[t("Atom releases page"),o(n)]),t(" to download the "),ie,t(" file explicitly. Once you have that file, you can click on it to extract the application and then drag the new "),se,t(' application into your "Applications" folder.')]),le,re,de,ce,me]),tab2:a(({title:i,value:s,isActive:l})=>[he,e("p",null,[t("Atom is available with Windows installers that can be downloaded from https://atom.io or from the "),e("a",ue,[t("Atom releases page"),o(n)]),t(". Use "),pe,t(" for 32-bit systems and "),ge,t(" for 64-bit systems. This setup program will install Atom, add the "),fe,t(" and "),be,t(" commands to your "),ye,t(", and create shortcuts on the desktop and in the start menu.")]),we,ve,_e]),_:1}),ke,xe,o(c,{id:"206",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"installing-atom"},{tab0:a(({title:i,value:s,isActive:l})=>[e("p",null,[t("If you are using Atom's official package repositories, use your distribution's package manager to update Atom. Otherwise, you will need to manually download and install the latest "),Ae,t(" or "),Se,t(" package from https://atom.io. For more details, see "),e("a",Te,[t("Installing Atom on Linux."),o(n)])])]),tab1:a(({title:i,value:s,isActive:l})=>[e("p",null,[t('"Automatically Update" is enabled by default in Core Settings of the '),e("a",Ce,[t("Settings View"),o(n)]),t(", which will allow Atom to check for updates automatically. If you disable this setting you can update Atom manually.")]),Ie,e("ul",null,[Pe,e("li",null,[t("Search for "),Oe,t(" in the "),e("a",ze,[t("Command Palette"),o(n)]),t(" and click the "),Fe,t(" button.")])]),$e]),tab2:a(({title:i,value:s,isActive:l})=>[e("p",null,[t('"Automatically Update" is enabled by default in Core Settings of the '),e("a",Ye,[t("Settings View"),o(n)]),t(", which will allow Atom to check for updates automatically. If you disable this setting you can update Atom manually.")]),Ee,e("ul",null,[Ne,e("li",null,[t("Search for "),We,t(" in the "),e("a",Ue,[t("Command Palette"),o(n)]),t(" and click the "),qe,t(" button.")])]),Ve]),_:1}),je,Me,e("p",null,[t("To setup Atom in portable mode download the "),e("a",Le,[t("zip/tar.gz package for your system"),o(n)]),t(" and extract it to your removable storage.")]),o(c,{id:"268",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"installing-atom"},{tab0:a(({title:i,value:s,isActive:l})=>[Be,De]),tab1:a(({title:i,value:s,isActive:l})=>[He,Ge]),tab2:a(({title:i,value:s,isActive:l})=>[Re,Je]),_:1}),Ke,e("p",null,[t("The "),o(d,{to:"/hacking-atom/sections/hacking-on-atom-core/"},{default:a(()=>[t("Hacking on Atom Core")]),_:1}),t(" section of the flight manual covers instructions on how to clone and build the source code if you prefer that option.")]),Xe,e("p",null,[t("You can find definitions for all of the various terms that we use throughout the manual in our "),o(d,{to:"/resources/sections/glossary/"},{default:a(()=>[t("Glossary")]),_:1}),t(".")]),Ze,e("template",null,[Qe,e("p",null,[t("In "),o(d,{to:"/sections/basic-customization/"},{default:a(()=>[t("Basic Customization")]),_:1}),t(" we will see how to set different wrap preferences for different types of files (for example, if you want to wrap Markdown files but not other files).")]),et,e("p",null,[t("Another way to open a file in Atom is from the command line using the "),tt,t(" command. "),e("span",ot,[t('The Atom menu bar has a command named "Install Shell Commands" which installs the '),at,t(" and "),nt,t(" commands "),o(d,{to:"/installing-atom/#installing-atom-on-mac"},{default:a(()=>[t("if Atom wasn't able to install them itself")]),_:1}),t(" .")]),e("span",it,[t("The "),st,t(" and "),lt,t(" commands are installed automatically as a part of Atom's "),o(d,{to:"/sections/installing-atom/"},{default:a(()=>[t("installation process")]),_:1}),t(".")])]),rt,e("p",null,[t("Editing a file is pretty straightforward. You can click around and scroll with your mouse and type to change the content. There is no special editing mode or key commands. If you prefer editors with modes or more complex key commands, you should take a look at the "),e("a",dt,[t("Atom package list"),o(n)]),t(". There are a lot of packages that emulate popular styles.")]),ct,e("p",null,[t("The fuzzy finder uses the "),mt,t(", "),ht,t(" and "),ut,t(" configuration settings to filter out files and folders that will not be shown. If you have a project with tons of files you don't want it to search through, you can add patterns or paths to either of these config settings or your "),e("a",pt,[t("standard "),gt,t(" files"),o(n)]),t(". We'll learn more about config settings in "),o(d,{to:"/using-atom/sections/basic-customization/#global-configuration-settings"},{default:a(()=>[t("Global Configuration Settings")]),_:1}),t(", but for now you can easily set these in the Settings View under Core Settings.")]),e("p",null,[t("Both "),ft,t(" and "),bt,t(" are interpreted as glob patterns as implemented by the "),e("a",yt,[t("minimatch Node module"),o(n)]),t(".")]),wt])])}const It=S(I,[["render",vt],["__file","index.html.vue"]]);export{It as default}; diff --git a/assets/index.html.85a58c87.js b/assets/index.html.85a58c87.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.85a58c87.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.8656dbb7.js b/assets/index.html.8656dbb7.js new file mode 100644 index 0000000000..d85ccef767 --- /dev/null +++ b/assets/index.html.8656dbb7.js @@ -0,0 +1 @@ +import{_ as a,o as e,c as i,f as s}from"./app.0e1565ce.js";const t={},n=s('Here you will learn everything you need to know about actually using Pulsar. This introduces what Pulsar is, how to install it and how to use its basic features.
This section is all about the various functions built right into Pulsar and how you can use them to get the most out of it.
It will teach you everything from cursor movement all the way to basic customization of the application.
This is really what Pulsar is all about, here you will get to grips with what lies under the skin of Pulsar so you can begin hacking on it to extend and customize its functionality.
Here you will get into the real heart of Pulsar, where you get into the details of various internal APIs and full details of how to maintain packages.
Frequently asked questions. This is where you can find some of the more common questions and answers regarding Pulsar.
',11),o=[n];function r(h,l){return e(),i("div",null,o)}const u=a(t,[["render",r],["__file","index.html.vue"]]);export{u as default}; diff --git a/assets/index.html.89530304.js b/assets/index.html.89530304.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.89530304.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.8a1f00a1.js b/assets/index.html.8a1f00a1.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.8a1f00a1.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.8df00eba.js b/assets/index.html.8df00eba.js new file mode 100644 index 0000000000..02a543ef40 --- /dev/null +++ b/assets/index.html.8df00eba.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6e0db890","path":"/docs/atom-archive/resources/","title":"Appendix A : Resources","lang":"en-US","frontmatter":{"title":"Appendix A : Resources","sitemap":{"priority":0.1}},"excerpt":"","headers":[{"level":2,"title":"Resources","slug":"resources","link":"#resources","children":[{"level":3,"title":"Glossary","slug":"glossary","link":"#glossary","children":[]}]}],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":0.22,"words":66},"filePathRelative":"docs/atom-archive/resources/index.md"}');export{e as data}; diff --git a/assets/index.html.8ea55446.js b/assets/index.html.8ea55446.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.8ea55446.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.90c99035.js b/assets/index.html.90c99035.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.90c99035.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.941947fb.js b/assets/index.html.941947fb.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.941947fb.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.956b9123.js b/assets/index.html.956b9123.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.956b9123.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.96af6444.js b/assets/index.html.96af6444.js new file mode 100644 index 0000000000..9ba60c605b --- /dev/null +++ b/assets/index.html.96af6444.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-3e3eb0e0","path":"/docs/launch-manual/sections/faq/","title":"FAQ","lang":"en-US","frontmatter":{"lang":"en-US","title":"FAQ","description":"Frequently asked questions"},"excerpt":"","headers":[{"level":2,"title":"Having trouble?","slug":"having-trouble","link":"#having-trouble","children":[]},{"level":2,"title":"Common Issues","slug":"common-issues","link":"#common-issues","children":[{"level":3,"title":"macOS error \\"App is damaged and can\u2019t be opened\\"","slug":"macos-error-app-is-damaged-and-can-t-be-opened","link":"#macos-error-app-is-damaged-and-can-t-be-opened","children":[]},{"level":3,"title":"Pulsar does not launch \\"GPU process isn't usable. Goodbye\\"","slug":"pulsar-does-not-launch-gpu-process-isn-t-usable-goodbye","link":"#pulsar-does-not-launch-gpu-process-isn-t-usable-goodbye","children":[]}]}],"git":{"updatedTime":1694141555000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":0.18,"words":54},"filePathRelative":"docs/launch-manual/sections/faq/index.md"}`);export{e as data}; diff --git a/assets/index.html.9c7c1197.js b/assets/index.html.9c7c1197.js new file mode 100644 index 0000000000..bcfba196dd --- /dev/null +++ b/assets/index.html.9c7c1197.js @@ -0,0 +1 @@ +import{_ as n,o as s,c as l,a as e,d as o,w as i,b as a,r}from"./app.0e1565ce.js";const c={},d=e("h1",{id:"core-pulsar-packages-documentation",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#core-pulsar-packages-documentation","aria-hidden":"true"},"#"),a(" Core Pulsar Packages Documentation")],-1),u=e("p",null,"This aims to be a collection of information about the first-party packages published for Pulsar.",-1),h=e("p",null,"Secondly, and currently, it is an archive of every Wiki that was published from the upstream Atom Organization.",-1),_=e("p",null,"This means some of this information may be old, or no longer relevant. If it is, it has been marked as so at the top of the file under a warning.",-1),p=e("p",null,"None-the-less the information can still be helpful, and may prove to be valuable. Which is why it has been archived here.",-1),m=e("h2",{id:"archived-wikis",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#archived-wikis","aria-hidden":"true"},"#"),a(" Archived Wikis")],-1);function f(k,g){const t=r("RouterLink");return s(),l("div",null,[d,u,h,_,p,m,e("ul",null,[e("li",null,[o(t,{to:"/docs/packages/core/atom-languageclient/"},{default:i(()=>[a("Atom-LanguageClient")]),_:1})]),e("li",null,[o(t,{to:"/docs/packages/core/autocomplete-plus/"},{default:i(()=>[a("Autocomplete-Plus")]),_:1})]),e("li",null,[o(t,{to:"/docs/packages/core/github/"},{default:i(()=>[a("GitHub")]),_:1})]),e("li",null,[o(t,{to:"/docs/packages/core/ide-java/"},{default:i(()=>[a("IDE-Java")]),_:1})])])])}const v=n(c,[["render",f],["__file","index.html.vue"]]);export{v as default}; diff --git a/assets/index.html.a5b4ee80.js b/assets/index.html.a5b4ee80.js new file mode 100644 index 0000000000..cfa77bc1c2 --- /dev/null +++ b/assets/index.html.a5b4ee80.js @@ -0,0 +1 @@ +import{_ as r,o as i,c as d,a,b as e,d as o,w as c,f as u,r as t}from"./app.0e1565ce.js";const h={},l=u('Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
If you have any issues then please feel free to ask for help from the Pulsar Team or the wider community via any of our Community areas
',4),p={href:"https://github.com/pulsar-edit/pulsar/issues",target:"_blank",rel:"noopener noreferrer"},m={href:"https://github.com/pulsar-edit/pulsar/issues/new?assignees=&labels=bug%2Ctriage&projects=&template=bug-report.yml",target:"_blank",rel:"noopener noreferrer"},f=a("h2",{id:"common-issues",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#common-issues","aria-hidden":"true"},"#"),e(" Common Issues")],-1),b=a("h3",{id:"macos-error-app-is-damaged-and-can-t-be-opened",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#macos-error-app-is-damaged-and-can-t-be-opened","aria-hidden":"true"},"#"),e(' macOS error "'),a("em",null,"App is damaged and can\u2019t be opened"),e('"')],-1),_=a("code",null,"xattr -cr /Applications/Pulsar.app/",-1),g={href:"https://appletoolbox.com/app-is-damaged-cannot-be-opened-mac/",target:"_blank",rel:"noopener noreferrer"},y=a("h3",{id:"pulsar-does-not-launch-gpu-process-isn-t-usable-goodbye",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#pulsar-does-not-launch-gpu-process-isn-t-usable-goodbye","aria-hidden":"true"},"#"),e(' Pulsar does not launch "'),a("em",null,"GPU process isn't usable. Goodbye"),e('"')],-1),x=a("code",null,"--no-sandbox",-1),k={href:"https://github.com/pulsar-edit/pulsar/issues/174",target:"_blank",rel:"noopener noreferrer"};function v(w,C){const n=t("ExternalLinkIcon"),s=t("RouterLink");return i(),d("div",null,[l,a("p",null,[e("If you think you have found a bug then please have a look through our existing "),a("a",p,[e("issues"),o(n)]),e(" and if you can't find anything then please "),a("a",m,[e("create a new bug report"),o(n)]),e(".")]),f,b,a("p",null,[e("The binary is likely from before macOS binaries started being signed. A up-to-date and signed binary may be downloaded from "),o(s,{to:"/download.html"},{default:c(()=>[e("the download page")]),_:1}),e(". The unsigned binary can be made to run by running "),_,e(" in the terminal. See "),a("a",g,[e("here"),o(n)]),e(" for more information.")]),y,a("p",null,[e("You may need to launch the application with the argument "),x,e(" to get around this issue. This is something "),a("a",k,[e("under investigation"),o(n)]),e(".")])])}const I=r(h,[["render",v],["__file","index.html.vue"]]);export{I as default}; diff --git a/assets/index.html.a8a660db.js b/assets/index.html.a8a660db.js new file mode 100644 index 0000000000..2c0ee24e19 --- /dev/null +++ b/assets/index.html.a8a660db.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-31f54a17","path":"/tag/package-manager/","title":"package manager Tag","lang":"en-US","frontmatter":{"title":"package manager Tag","blog":{"type":"category","name":"package manager","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{a as data}; diff --git a/assets/index.html.a9c04ba9.js b/assets/index.html.a9c04ba9.js new file mode 100644 index 0000000000..6a43e5182c --- /dev/null +++ b/assets/index.html.a9c04ba9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-145ac574","path":"/blog/","title":"Articles","lang":"en-US","frontmatter":{"title":"Articles","blog":{"type":"type","key":"article"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.ad5774dc.js b/assets/index.html.ad5774dc.js new file mode 100644 index 0000000000..79cee55574 --- /dev/null +++ b/assets/index.html.ad5774dc.js @@ -0,0 +1,54 @@ +import{_ as o,o as i,c as l,a as s,b as e,d as a,f as t,r as c}from"./app.0e1565ce.js";const d={},r=t('STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
In Atom 1.13
the Shadow DOM got removed from text editors. Find a guide how to migrate your theme or package in this appendix.
Here is a quick summary to see all the changes at a glance:
Before | +/- | After |
---|---|---|
atom-text-editor::shadow {} | - ::shadow | atom-text-editor {} |
.class /deep/ .class {} | - /deep/ | .class .class {} |
atom-text-editor, :host {} | - :host | atom-text-editor {} |
.comment {} | + .syntax-- | .syntax--comment {} |
Below you'll find more detailed examples.
::shadow
Remove the ::shadow
pseudo-element selector from atom-text-editor
.
Before:
atom-text-editor::shadow .cursor {
+ border-color: hotpink;
+}
+
After:
atom-text-editor .cursor {
+ border-color: hotpink;
+}
+
/deep/
Remove the /deep/
combinator selector. It didn't get used that often, mainly to customize the scrollbars.
Before:
.scrollbars-visible-always /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
After:
.scrollbars-visible-always ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
:host
Remove the :host
pseudo-element selector. To scope any styles to the text editor, using atom-text-editor
is already enough.
Before:
atom-text-editor,
+:host {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
After:
atom-text-editor {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
syntax--
Add a syntax--
prefix to all grammar selectors used by language packages. It looks a bit verbose, but it will protect all the grammar selectors from accidental style pollution.
Before:
.comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .markup.link {
+ color: @mono-3;
+ }
+}
+
After:
.syntax--comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .syntax--markup.syntax--link {
+ color: @mono-3;
+ }
+}
+
Note: Selectors like the .gutter
, .indent-guide
, .cursor
among others, that are also part of Syntax themes, don't need a prefix. Only grammar selectors that get used by language packages. For example .syntax--keyword
, .syntax--keyword.syntax--operator.syntax--js
.
Atom also allowed you to target a specific shadow DOM context with an entire style sheet by adding .atom-text-editor
to the file name. This is now not necessary anymore and can be removed.
Before:
my-ui-theme/
+ styles/
+ index.atom-text-editor.less
+
After:
my-ui-theme/
+ styles/
+ index.less
+
By replacing atom-text-editor::shadow
with atom-text-editor.editor
, specificity might have changed. This can cause the side effect that some of your properties won't be strong enough. To fix that, you can increase specificity on your selector. A simple way is to just repeat your class (in the example below it's .my-class
):
Before:
atom-text-editor::shadow .my-class {
+ color: hotpink;
+}
+
After:
atom-text-editor .my-class.my-class {
+ color: hotpink;
+}
+
git clone https://github.com/pulsar-edit/pulsar-edit.github.io
+
+cd pulsar-edit.github.io
+
+pnpm install
+
Once installed there are a number of scripts available to help you develop and build the site. Just prefix each command with pnpm
. e,g, pnpm dev
.
dev
Starts a watch task that will rebuild VuePress whenever a change has been made to the included Markdown and JavaScript files. Additionally, it launches the development server to test the results in the browser.
build
Creates an optimized production build.
format
Note: This task will run automatically on every commit, so it can be ignored in most cases
lint
Lints all Markdown files in the repository.
Note: This task will run automatically on every commit, so it can be ignored in most cases
Whilst dev
does run a watch task, not everything will be updated and some changes will require you to shut down the server and start it again. For example adding @include
files to another file will not rebuild automatically.
If you wish to add new files from another repository via alias or @include
then you will need to run pnpm update
to get the latest version of the repository - the pnpm-lock.yml
file will also be updated and must be part of any commit.
Nearly everything regarding the configuration of the website itself is controlled via the files found in the /docs/.vuepress
directory.
Currently we have three main configuration files.
config.js
This is the main configuration file for the website. This controls everything from the available settings, additional VuePress plugins, website description and various other elements to control various settings and plugins.
',12),A={href:"https://v2.vuepress.vuejs.org/reference/config.html",target:"_blank",rel:"noopener noreferrer"},S={href:"https://vuepress-theme-hope.github.io/v2/config/",target:"_blank",rel:"noopener noreferrer"},q=e("p",null,[t("This file is broken down to to keep it tidy, the below files are imported to "),e("code",null,"config.js"),t(" to extend the configuration file without making it unwieldy.")],-1),I=e("h3",{id:"navbar-js",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#navbar-js","aria-hidden":"true"},"#"),t(),e("code",null,"navbar.js")],-1),W=e("p",null,"This controls the layout for the links in the top middle of the page and is always displayed.",-1),C=e("p",null,"Items that go here are ones that we always want to be shown and should always be available for quick navigation.",-1),M=e("p",null,"Each object can have a number of different values. The main ones we use are:",-1),N=e("li",null,[e("code",null,"text"),t(": This sets the text for the label.")],-1),O=e("code",null,"icon",-1),H={href:"https://fontawesome.com/",target:"_blank",rel:"noopener noreferrer"},E=e("code",null,"fa-",-1),F={href:"https://fontawesome.com/icons/house?s=solid&f=classic",target:"_blank",rel:"noopener noreferrer"},P=e("code",null,"solid fa-house",-1),Y=e("li",null,[e("code",null,"link"),t(": This controls where the link will actually take you. This can be a relative reference internal to the website or can be a URL to an external site.")],-1),U=e("li",null,[e("code",null,"children"),t(": Allows you to specify an array of child objects which will appear as a dropdown on mouseover. Use of this disables the "),e("code",null,"link"),t(" value. Each child can be defined as a full object as described here or can simply be a relative link from which the text will be set by the YAML title.")],-1),V={href:"https://v2.vuepress.vuejs.org/reference/default-theme/config.html#navbar",target:"_blank",rel:"noopener noreferrer"},B={href:"https://vuepress-theme-hope.github.io/v2/config/theme/layout.html#navbar-config",target:"_blank",rel:"noopener noreferrer"},D=e("h3",{id:"sidebar-js",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#sidebar-js","aria-hidden":"true"},"#"),t(),e("code",null,"sidebar.js")],-1),R=e("p",null,[t("This control what is displayed in the sidebar on the left of the website. It is not displayed globally, only on directories which are set within the sidebar, currently we only have "),e("code",null,"docs"),t(" configured.")],-1),z=e("p",null,[t("Like "),e("code",null,"navbar.js"),t(" each sidebare item is configured as an object with a number of different values.")],-1),G=e("li",null,[e("code",null,"text"),t(": This sets the text for the label.")],-1),X=e("li",null,[e("code",null,"link"),t(": Controls the relative link for navigating the documents within the section")],-1),J=e("code",null,"icon",-1),Q={href:"https://fontawesome.com/",target:"_blank",rel:"noopener noreferrer"},K=e("code",null,"fa-",-1),Z={href:"https://fontawesome.com/icons/house?s=solid&f=classic",target:"_blank",rel:"noopener noreferrer"},$=e("code",null,"solid fa-house",-1),ee=e("li",null,[e("code",null,"prefix"),t(": This adds a file path prefix to the item so its children do not need to specify the full path.")],-1),te=e("code",null,"collapsable",-1),oe=e("em",null,"Note",-1),ne=e("code",null,"collapsible",-1),ie={href:"https://github.com/vuepress-theme-hope/vuepress-theme-hope/issues/2377",target:"_blank",rel:"noopener noreferrer"},se=e("li",null,[e("code",null,"children"),t(": Takes an array of objects configured as above. Can also be set as a simple relative link in which case the title will be the YAML title of the document it links to.")],-1),ae={href:"https://v2.vuepress.vuejs.org/reference/default-theme/config.html#sidebar",target:"_blank",rel:"noopener noreferrer"},re={href:"https://vuepress-theme-hope.github.io/v2/config/theme/layout.html#sidebar-config",target:"_blank",rel:"noopener noreferrer"},le=i(`Within the styles/
directory you will find the .scss file for controlling various aspects of the website's theme.
TODO: This will be updated when we actually modify these files extensively.
One of the most important things to take note of when adding new documentation is where it should go within the website layout.
The generalized overall layout of the website looks like this:
docs
+\u251C\u2500\u2500 root level .md files
+\u2514\u2500\u2500 section folder
+ \u251C\u2500\u2500 sections
+ \u2514\u2500\u2500 index.md
+
The general idea is that for files that can stand by themselves (for example the About Us
page, Repositories
etc.) they exist at the docs/
"root" level.
For anything that is more complex it needs to have a section directory named appropriately, an index.md
file within it and a sections
directory.
index.md
This index file needs to have a YAML frontmatter to define, at a minimum, the title of the document. This is displayed as an H1
header for the page (note: subsequent H1
headers will be ignored so always start at H2
).
The rest of this index file will be used to display the actual content you want to show. This is done in a number of ways.
First of all you can just include standard markdown. This is often used for introducing the section or adding one of our reusable components (e.g. a danger
container).
The rest of the file should consist of @includes
which take data from other folders on the website and integrates it automatically. Usually this will be the sections
files which will be covered next.
e.g.
@include(sections/file-organization.md)
+
This is done by having a value defined on the config.js
file which will provide an alias for us to use:
if (file.startsWith("@orgdocs")) {
+ return file.replace(
+ "@orgdocs",
+ path.resolve(__dirname, "../../node_modules/.github/")
+ );
+}
+
This allows us to include org-level docs by using this special alias.
e.g.
@include(@orgdocs/TOOLING.md)
+
The sections
directory is where we include the rest of the documents broken down by section. These should be self contained files which can be used alone but are designed to be included on the section page. This approach allows us flexibility with ordering as well as including these files in other places without needing to duplicate the material.
Files here can be navigated to directly on the website but should not be linked to directly.
These files shoud not have any YAML frontmatter as they will be included and shown as text.
An alias for this exists in config.js
to access files from this repository.
alias: {
+ '@images': path.resolve(__dirname, '../../node_modules/.github/images')
+},
+
So to include an image you simply need to use the standard markdown image link along with the alias:
e.g.
![MyImage](@images/path/to/image.png)
+
The documentation for the project should maintain a consistent style where possible. This covers a number of aspects such as writing style, naming conventions, folder structure, links etc.
All docs are currently in American English (en-US) but localization is planned.
The main structure for documentation can be seen in docs/docs
. There are a number of "root" sections:
launch-manual
- For the current main Pulsar documentationpackages
- Currently holds wiki info from the atom package reposresources
- For other referenced docsblog
- For the website blogatom-archive
- For "as is" archived Atom documentationWithin each section is an index.md
which will usually contain info about each sub-section as well as links to them. These correspond to the second level items on the sidebar.
Inside sections
are the sub-sections which group more specific topics. These also have an index.md
which corresponds to the third level item on the sidebar. This file is displayed on the website as a single long documenent but is actually created from a number of @include() lines which reference individual sections within the next sections directory. These should be relative to the location of index.md e.g. @include(sections/pulsar-packages.md). This file also contains the frontmatter for defining the title, language and description of the file and should also be the first level heading for the page. Here is also where you can place a container such as Under Construction
to apply to the entire page.
Inside the next sections
directory should be the actual content of the document. Each section should start with a second level header and should not contain any frontmatter.
Internal links can just be to the header (e.g.[Structure](#structure)
), this to all sections included on the parent index.md
so care should be made to not create any duplicate headers.
All other links should be relative but do not need to reference the index file itself (e.g.[Installing](../getting-started#installing)
) will find the heading #installing
within the index file in the getting-started
directory above.
Images should be added to [pulsar-assets](https://github.com/pulsar-edit/pulsar-assets)
and referenced from the package imported from it. This is done via an alias on the .vuepress/config.js
file which adds most of the path for you: '@images': path.resolve(__dirname, '../../node_modules/pulsar-assets/images')
so the link to your image would just be ![myImage](@images/pulsar/myImage.png "My Image")
.
The name of the application is Pulsar
and should be capitalized as such. Whilst the website and GitHub org name is Pulsar-Edit
, this should not be used within documentation outside of links to the GitHub org or website.
Operating systems should be named as such:
Linux
- All GNU/Linux distributionsmacOS
- Apple's current operating system familyWindows
- Microsoft Windows family of operating systemsThis is also the order they should appear in within the tab switcher.
When using the #tabs
switcher they should be in this order.
When referencing them inline then they should be abbreviated to the following, strongly emphasized and separated by a -
:
To keep order consistent it should be LNX -> MAC -> WIN. If instructions: common to two then it should either be LNX/MAC, LNX/WIN -> MAC/WIN
For Linux we may sometimes need to reference particular distros or families of distributions. We currently use:
Ubuntu/Debian
for all distributions based on Debian or UbuntuFedora/RHEL
for all distrububtions based on Fedora Linux & Red Hat Red Hat Enterprise Linux. This includes AlmaLinux, CentOS, Rocky Linux etc.Arch
- for all Arch based distributions such as Manjaro, Garuda, ArcoLinux etc.OpenSUSE
- for all OpenSUSE based distributions such as GeckoLinuxWe may need to add more in the future but generally users of less popular or more technical distributions such as Gentoo or NixOS understand how to adapt to their OS from the instructions above.
Where you want to display an info
, warning
or tab/code switcher in the document you should use a container with the :::
syntax.
e.g.
::: tabs#filename
+
+@tab Linux
+
+Lorem ipsum dolor sit amet...
+
+@tab macOS
+
+Lorem ipsum dolor sit amet...
+
+@tab Windows
+
+Lorem ipsum dolor sit amet...
+
+:::
+
or
::: tip My Helpful Tip
+
+You might want to do X to get Y
+
+:::
+
YYYYMMDD-<author>-<title>.md
e.g 20221031-CreativeUsername-ThisIsMyBlogPost.md
title
, author
, date
, category
and tag
as the minimum as the others will default to false. title
- String: The displayed title of the post, consider this as H1
author
- String: The name of the author to be displayed.date
- Date (ISO 8601): Allows display of date as well as enabling sorting on the timeline, set to the same as your filename date but with hyphens (e.g. 2022-10-31
).category
- String (multiline): Enables filtering by category, this should be based on the subject of the post e.g. release
, dev log
, announcement
. This is a multiline field if you want to set more than one category.tag
- String (multiline): Enables filtering by tags, this should be based on the content of the post and areas it touches on e.g. website
, editor
, config
.sticky
- Bool: Enables "pinning" on thestar
- Bool: Enables use of the star
category for any important articles we want to remain visible.article
- Bool: You probably won't want to use this but setting this to false
will exclude this page from appearing. This is set on the "example" blog post intentionally.<!-- more -->
. Anything above the comment will be treated as the excerpt and anything underneath will be the content of the post. The website itself has a number of features which the aforementioned frontmatter fields can influence.
There is a "selector" at the top of the site which currently reads All
, Star
, Slides
, Encrypted
. The only ones we use are the first two and currently the theme doesn't support configuration of this. If necessary we may be able to hide it with CSS.
Any article with Star
set to true
will be shown in the star
category.
On the right bar are 4 counters/links and filters for Articles
, Category
, Tag
and Timeline
.Category
and Tag
are used to filter articles depending on the categories submitted in the respective frontmatter fields. The Timeline
allows views of blog posts over time according to the dates set in the date
frontmatter field.
Just ask in Discord or GH Discussions and ping the @documentation
team.
This is very much barebones default config so please let us know if you have any suggestions as to how we can improve it.
',7);function Ie(We,Ce){const n=s("ExternalLinkIcon"),a=s("RouterLink");return l(),d("div",null,[u,e("p",null,[t("The website itself is built using "),e("a",p,[t("VuePress v2"),o(n)]),t(" and the "),e("a",m,[t("Vuepress Hope Theme"),o(n)]),t(".")]),b,f,g,e("ul",null,[e("li",null,[t("Node.js - at least version 16 (recommended installation via "),e("a",v,[t("nvm"),o(n)]),t(").")]),w,e("li",null,[e("a",y,[t("pnpm"),o(n)]),t(" - simply run "),_])]),k,e("p",null,[t("The website repository is "),e("a",x,[t("https://github.com/pulsar-edit/pulsar-edit.github.io"),o(n)]),t(". Other assets are stored on other repositories but these will be downloaded automatically.")]),T,e("p",null,[t("Runs "),e("a",j,[t("Prettier"),o(n)]),t(" over all Markdown files included in the repository to ensure consistent formatting.")]),L,e("p",null,[t("For a full reference you can look at the documentation for "),e("a",A,[t("VuePress"),o(n)]),t(" and the "),e("a",S,[t("Hope Theme"),o(n)]),t(".")]),q,I,W,C,M,e("ul",null,[N,e("li",null,[O,t(": Used to prefix an icon to the item. The theme supports the free "),e("a",H,[t("FontAwesome"),o(n)]),t(" font natively. To add an icon you need to specify its name without the first "),E,t(" e.g. "),e("a",F,[t("fa-house"),o(n)]),t(" would be specified as "),P,t(".")]),Y,U]),e("p",null,[t("For a full reference you can look at the documentation for "),e("a",V,[t("VuePress"),o(n)]),t(" and the "),e("a",B,[t("Hope Theme"),o(n)]),t(".")]),D,R,z,e("ul",null,[G,X,e("li",null,[J,t(": Used to prefix an icon to the item. The theme supports the free "),e("a",Q,[t("FontAwesome"),o(n)]),t(" font natively. To add an icon you need to specify its name without the first "),K,t(" e.g. "),e("a",Z,[t("fa-house"),o(n)]),t(" would be specified as "),$,t(".")]),ee,e("li",null,[te,t(": (sic) Controls whether the item can be collapsed. "),oe,t(": This a breaking change on a future version of the Hope Theme so will need to be renamed "),ne,t(" when updated, see: "),e("a",ie,[t("Issue"),o(n)]),t(".")]),se]),e("p",null,[t("For a full reference you can look at the documentation for "),e("a",ae,[t("VuePress"),o(n)]),t(" and the "),e("a",re,[t("Hope Theme"),o(n)]),t(".")]),le,e("p",null,[t("However you can also use "),de,t(" to feature files from a different section of the website or even files from outside the main site. We use this to include files which are maintained on the organization "),e("a",ce,[t(".github repo"),o(n)]),t(" for org-level documents.")]),he,e("p",null,[t("Assets should be uploaded to the "),e("a",ue,[t(".github repo"),o(n)]),t(" repository so they can be used org-wide.")]),pe,e("p",null,[t("You can also find a list of currently maintained preformatted containers for various purposes at "),e("a",me,[t("pulsar-edit.github.io/common-text-blocks.md"),o(n)]),t(".")]),e("p",null,[t("See "),e("a",be,[t("VuePress Hope documentation"),o(n)])]),fe,ge,ve,e("p",null,[t("This is a guide on how to add a blog post to the website which will be shown on "),e("a",we,[t("https://pulsar-edit.dev/article/"),o(n)]),t(".")]),e("p",null,[t("We are using the "),e("a",ye,[t("Vuepress Blog Plugin"),o(n)]),t(" which comes as part of our Vuepress Hope Theme with some light configuration to suit our purposes.")]),e("p",null,[t("This is all implemented in the main "),e("a",_e,[t("website repository"),o(n)]),t(".")]),ke,e("ul",null,[xe,e("li",null,[t("The rest of the post is just standard Markdown - we are currently only supporting the standard features as per the "),e("a",Te,[t("MDEnhance"),o(n)]),t(" plugin but if we need more features such as GFM then please discuss and we can look at adding it to the website.")]),je,Le]),e("p",null,[t("See "),e("a",Ae,[t("example post"),o(n)]),t(" with everything above.")]),Se,e("p",null,[t("See "),o(a,{to:"/docs/resources/website/#building-the-website"},{default:c(()=>[t("building")]),_:1})]),qe])}const Ne=r(h,[["render",Ie],["__file","index.html.vue"]]);export{Ne as default}; diff --git a/assets/index.html.b8230621.js b/assets/index.html.b8230621.js new file mode 100644 index 0000000000..3cd09dfbdf --- /dev/null +++ b/assets/index.html.b8230621.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-0aa2ec94","path":"/docs/launch-manual/sections/behind-pulsar/","title":"Behind Pulsar","lang":"en-US","frontmatter":{"lang":"en-US","title":"Behind Pulsar","description":"Advanced Pulsar hacking"},"excerpt":"","headers":[{"level":2,"title":"Configuration API","slug":"configuration-api","link":"#configuration-api","children":[{"level":3,"title":"Reading Config Settings","slug":"reading-config-settings","link":"#reading-config-settings","children":[]},{"level":3,"title":"Writing Config Settings","slug":"writing-config-settings","link":"#writing-config-settings","children":[]}]},{"level":2,"title":"Keymaps In-Depth","slug":"keymaps-in-depth","link":"#keymaps-in-depth","children":[{"level":3,"title":"Structure of a Keymap File","slug":"structure-of-a-keymap-file","link":"#structure-of-a-keymap-file","children":[]},{"level":3,"title":"Removing Bindings","slug":"removing-bindings","link":"#removing-bindings","children":[]},{"level":3,"title":"Forcing Chromium's Native Keystroke Handling","slug":"forcing-chromium-s-native-keystroke-handling","link":"#forcing-chromium-s-native-keystroke-handling","children":[]},{"level":3,"title":"Overloading Key Bindings","slug":"overloading-key-bindings","link":"#overloading-key-bindings","children":[]},{"level":3,"title":"Step-by-Step: How Keydown Events are Mapped to Commands","slug":"step-by-step-how-keydown-events-are-mapped-to-commands","link":"#step-by-step-how-keydown-events-are-mapped-to-commands","children":[]},{"level":3,"title":"Overriding Pulsar's Keyboard Layout Recognition","slug":"overriding-pulsar-s-keyboard-layout-recognition","link":"#overriding-pulsar-s-keyboard-layout-recognition","children":[]}]},{"level":2,"title":"Scoped Settings, Scopes and Scope Descriptors","slug":"scoped-settings-scopes-and-scope-descriptors","link":"#scoped-settings-scopes-and-scope-descriptors","children":[{"level":3,"title":"Scope Names in Syntax Tokens","slug":"scope-names-in-syntax-tokens","link":"#scope-names-in-syntax-tokens","children":[]},{"level":3,"title":"Scope Selectors","slug":"scope-selectors","link":"#scope-selectors","children":[]},{"level":3,"title":"Scope Descriptors","slug":"scope-descriptors","link":"#scope-descriptors","children":[]}]},{"level":2,"title":"Serialization in Pulsar","slug":"serialization-in-pulsar","link":"#serialization-in-pulsar","children":[{"level":3,"title":"Package Serialization Hook","slug":"package-serialization-hook","link":"#package-serialization-hook","children":[]},{"level":3,"title":"Serialization Methods","slug":"serialization-methods","link":"#serialization-methods","children":[]},{"level":3,"title":"Versioning","slug":"versioning","link":"#versioning","children":[]}]},{"level":2,"title":"Developing Node Modules","slug":"developing-node-modules","link":"#developing-node-modules","children":[{"level":3,"title":"Linking a Node Module Into Your Pulsar Dev Environment","slug":"linking-a-node-module-into-your-pulsar-dev-environment","link":"#linking-a-node-module-into-your-pulsar-dev-environment","children":[]}]},{"level":2,"title":"Interacting With Other Packages Via Services","slug":"interacting-with-other-packages-via-services","link":"#interacting-with-other-packages-via-services","children":[]},{"level":2,"title":"Maintaining Your Packages","slug":"maintaining-your-packages","link":"#maintaining-your-packages","children":[{"level":3,"title":"Publishing a Package Manually","slug":"publishing-a-package-manually","link":"#publishing-a-package-manually","children":[]},{"level":3,"title":"Adding a Collaborator","slug":"adding-a-collaborator","link":"#adding-a-collaborator","children":[]},{"level":3,"title":"Transferring Ownership","slug":"transferring-ownership","link":"#transferring-ownership","children":[]},{"level":3,"title":"Unpublish Your Package","slug":"unpublish-your-package","link":"#unpublish-your-package","children":[]},{"level":3,"title":"Rename Your Package","slug":"rename-your-package","link":"#rename-your-package","children":[]}]},{"level":2,"title":"Summary","slug":"summary","link":"#summary","children":[]}],"git":{"updatedTime":1694141555000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4}]},"readingTime":{"minutes":0.51,"words":154},"filePathRelative":"docs/launch-manual/sections/behind-pulsar/index.md"}`);export{e as data}; diff --git a/assets/index.html.bb4bd5d9.js b/assets/index.html.bb4bd5d9.js new file mode 100644 index 0000000000..cbb0d6e199 --- /dev/null +++ b/assets/index.html.bb4bd5d9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0166a572","path":"/docs/packages/","title":"Packages","lang":"en-us","frontmatter":{"lang":"en-us","title":"Packages","description":"Documentation Resource for Packages"},"excerpt":"","headers":[{"level":2,"title":"Core Packages","slug":"core-packages","link":"#core-packages","children":[{"level":3,"title":"Updated","slug":"updated","link":"#updated","children":[]},{"level":3,"title":"Archived Documentation","slug":"archived-documentation","link":"#archived-documentation","children":[]}]},{"level":2,"title":"Community Packages","slug":"community-packages","link":"#community-packages","children":[]}],"git":{"updatedTime":1673840801000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":4},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":2}]},"readingTime":{"minutes":1.07,"words":322},"filePathRelative":"docs/packages/index.md"}');export{e as data}; diff --git a/assets/index.html.bb5ab2a9.js b/assets/index.html.bb5ab2a9.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.bb5ab2a9.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.be37943f.js b/assets/index.html.be37943f.js new file mode 100644 index 0000000000..f6114c0b8a --- /dev/null +++ b/assets/index.html.be37943f.js @@ -0,0 +1,188 @@ +import{_ as g,a as b,b as f,c as k}from"./unity-theme.516a5ae9.js";import{_ as y,a as v}from"./symbol.b88bf5a9.js";import{_ as w}from"./bookmarks.736c4e14.js";import{b as _,_ as C,a as x}from"./encodings.f93acf64.js";import{a as S,_ as P}from"./find-replace-project.ada882a7.js";import{_ as q,a as T}from"./snippet-scope.1f381b52.js";import{_ as A}from"./autocomplete.b0bae406.js";import{_ as p}from"./folding.6d09f8df.js";import{_ as u}from"./panes.a7cec271.js";import{_ as I}from"./allow-pending-pane-items.0fe3dd4a.js";import{_ as L}from"./grammar.c16b8cd6.js";import{_ as M,c as R,a as N,b as F,d as O,e as W}from"./open-on-github.6fa9b166.js";import{_ as H,a as G,b as D,c as j,d as Y,e as E,f as z,g as U,h as $,i as B,j as V,k as X,l as K,m as J,n as Z,o as Q,p as ee,q as te,r as ne,s as ae,t as oe,u as se,v as ie,w as le,x as re,y as ce}from"./github-review-reply.6d405e4b.js";import{_ as de,a as pe}from"./preview.2eaf146a.js";import{_ as ue,a as he,b as me,c as ge,d as be}from"./portable-mode-folder.90669a9a.js";import{_ as fe,o as ke,c as ye,a as e,b as t,d as n,w as o,e as h,f as r,r as d}from"./app.0e1565ce.js";const ve={},we=r('Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Pulsar, it will show up in the Settings View under the "Packages" tab, along with all the pre-installed packages that come with Pulsar. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Pulsar will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Pulsar from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on pulsar-edit.dev, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
Clicking on "Install" will install the theme and make it available in the Theme dropdowns as we saw in Changing the Theme.
You can also install packages or themes from the command line using ppm (Pulsar Package Manager). This is used by running pulsar -p <commmand>
or pulsar --package <command>
.
Tip
Check that you have ppm available by running the following command in your terminal:
$ pulsar -p help install
+
You should see a message print out with details about the pulsar -p install
command.
If you do not, see the Installing Pulsar section for instructions on how to install the pulsar
command for your system.
You can install packages by using the pulsar -p install
command:
pulsar -p install <package_name>
to install the latest version.pulsar -p install <package_name>@<package_version>
to install a specific version.You can also use ppm to find new packages to install. If you run pulsar -p search
, you can search the package registry for a search term.
$ pulsar -p search linter
+> Search Results For 'linter' (30)
+> \u251C\u2500\u2500 linter A Base Linter with Cow Powers (9863242 downloads, 4757 stars)
+> \u251C\u2500\u2500 linter-ui-default Default UI for the Linter package (7755748 downloads, 1201 stars)
+> \u251C\u2500\u2500 linter-eslint Lint JavaScript on the fly, using ESLint (v7 or older) (2418043 downloads, 1660 stars)
+> \u251C\u2500\u2500 linter-jshint Linter plugin for JavaScript, using jshint (1202044 downloads, 1271 stars)
+> \u251C\u2500\u2500 linter-gcc Lint C and C++ source files using gcc / g++ (863989 downloads, 194 stars)
+> ...
+> \u251C\u2500\u2500 linter-shellcheck Lint Bash on the fly, using shellcheck (136938 downloads, 280 stars)
+> \u2514\u2500\u2500 linter-rust Lint Rust-files, using rustc and/or cargo (132550 downloads, 91 stars)
+
You can use pulsar -p view
to see more information about a specific package.
$ pulsar -p view linter
+> linter
+> \u251C\u2500\u2500 3.4.0
+> \u251C\u2500\u2500 https://github.com/steelbrain/linter
+> \u251C\u2500\u2500 A Base Linter with Cow Powers
+> \u251C\u2500\u2500 9863242 downloads
+> \u2514\u2500\u2500 4757 stars
+>
+> Run \`pulsar -p install linter\` to install this package.
+
Pulsar can install from a GitHub repository or any valid git remote url. The -b
option can then be used to specify a particular tag or branch.
Git remotepulsar -p install <git_remote> [-b <branch_or_tag>]
GitHubpulsar -p install <github_username>/<github_project> [-b <branch_or_tag>]
pulsar -p install https://github.com/mauricioszabo/generic-lsp/
or
pulsar -p install mauricioszabo/generic-lsp
This will use the current HEAD commit of the default branch. If you want to install a specific version from a branch or tag then use the -b
option:
e.g. pulsar -p install https://github.com/mauricioszabo/generic-lsp/ -b v1.0.3
While it's pretty easy to move around Pulsar by clicking with the mouse or using the arrow keys, there are some keybindings that may help you keep your hands on the keyboard and navigate around a little faster.
',7),De=e("p",null,"Pulsar has support for all the standard Linux cursor movement key combinations. To go up, down, left or right a single character you can use the arrow keys.",-1),je=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),Ye=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Left"),t(" - Move to the beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Right"),t(" - Move to the end of word")]),e("li",null,[e("kbd",null,"Home"),t(" - Move to the first character of the current line")]),e("li",null,[e("kbd",null,"End"),t(" - Move to the end of the line")]),e("li",null,[e("kbd",null,"Ctrl+Home"),t(" - Move to the top of the file")]),e("li",null,[e("kbd",null,"Ctrl+End"),t(" - Move to the bottom of the file")])],-1),Ee=e("p",null,[t("Pulsar ships with many of the basic Emacs keybindings for navigating a document. To go up and down a single character, you can use "),e("kbd",null,"Ctrl+P"),t(" and "),e("kbd",null,"Ctrl+N"),t(". To go left and right a single character, you can use "),e("kbd",null,"Ctrl+B"),t(" and "),e("kbd",null,"Ctrl+F"),t(". These are the equivalent of using the arrow keys, though some people prefer not having to move their hands to where the arrow keys are located on their keyboard.")],-1),ze=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),Ue=e("ul",null,[e("li",null,[e("kbd",null,"Alt+Left"),t(" or "),e("kbd",null,"Alt+B"),t(" - Move to the beginning of word")]),e("li",null,[e("kbd",null,"Alt+Right"),t(" or "),e("kbd",null,"Alt+F"),t(" - Move to the end of word")]),e("li",null,[e("kbd",null,"Cmd+Left"),t(" or "),e("kbd",null,"Ctrl+A"),t(" - Move to the first character of the current line")]),e("li",null,[e("kbd",null,"Cmd+Right"),t(" or "),e("kbd",null,"Ctrl+E"),t(" - Move to the end of the line")]),e("li",null,[e("kbd",null,"Cmd+Up"),t(" - Move to the top of the file")]),e("li",null,[e("kbd",null,"Cmd+Down"),t(" - Move to the bottom of the file")])],-1),$e=e("p",null,"Pulsar has support for all the standard Windows cursor movement key combinations. To go up, down, left or right a single character you can use the arrow keys.",-1),Be=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),Ve=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Left"),t(" - Move to the beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Right"),t(" - Move to the end of word")]),e("li",null,[e("kbd",null,"Home"),t(" - Move to the first character of the current line")]),e("li",null,[e("kbd",null,"End"),t(" - Move to the end of the line")]),e("li",null,[e("kbd",null,"Ctrl+Home"),t(" - Move to the top of the file")]),e("li",null,[e("kbd",null,"Ctrl+End"),t(" - Move to the bottom of the file")])],-1),Xe=r('You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
Pulsar also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the Command Palette, but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your keymap.cson
to create a key combination. You can open keymap.cson
file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu. For example, the command editor:move-to-beginning-of-screen-line
is available in the command palette, but it's not bound to any key combination. To create a key combination you need to add an entry in your keymap.cson
file. For editor:select-to-previous-word-boundary
, you can add the following to your keymap.cson
:
This will bind the command editor:select-to-previous-word-boundary
to LNX/WIN: Ctrl+Shift+E - MAC: Cmd+Shift+E. For more information on customizing your keybindings, see Customizing Keybindings.
Here's a list of Movement and Selection Commands that do not have a keyboard shortcut by default:
',2),et=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`editor:move-to-beginning-of-next-paragraph +editor:move-to-beginning-of-previous-paragraph +editor:move-to-beginning-of-screen-line +editor:move-to-beginning-of-line +editor:move-to-end-of-line +editor:move-to-first-character-of-line +editor:move-to-beginning-of-next-word +editor:move-to-previous-word-boundary +editor:move-to-next-word-boundary +editor:select-to-beginning-of-next-paragraph +editor:select-to-beginning-of-previous-paragraph +editor:select-to-end-of-line +editor:select-to-beginning-of-line +editor:select-to-beginning-of-next-word +editor:select-to-next-word-boundary +editor:select-to-previous-word-boundary +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),tt=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`editor:move-to-beginning-of-next-paragraph +editor:move-to-beginning-of-previous-paragraph +editor:move-to-beginning-of-screen-line +editor:move-to-beginning-of-line +editor:move-to-beginning-of-next-word +editor:move-to-previous-word-boundary +editor:move-to-next-word-boundary +editor:select-to-beginning-of-next-paragraph +editor:select-to-beginning-of-previous-paragraph +editor:select-to-beginning-of-line +editor:select-to-beginning-of-next-word +editor:select-to-next-word-boundary +editor:select-to-previous-word-boundary +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),nt=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`editor:move-to-beginning-of-next-paragraph +editor:move-to-beginning-of-previous-paragraph +editor:move-to-beginning-of-screen-line +editor:move-to-beginning-of-line +editor:move-to-end-of-line +editor:move-to-first-character-of-line +editor:move-to-beginning-of-next-word +editor:move-to-previous-word-boundary +editor:move-to-next-word-boundary +editor:select-to-beginning-of-next-paragraph +editor:select-to-beginning-of-previous-paragraph +editor:select-to-end-of-line +editor:select-to-beginning-of-line +editor:select-to-beginning-of-next-word +editor:select-to-next-word-boundary +editor:select-to-previous-word-boundary +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),at=r('You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press LNX/WIN: Ctrl+R - MAC: Cmd+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to LNX/WIN: Ctrl+T - MAC: Cmd+T. You can also search for symbols across your project but it requires a tags
file.
Pulsar also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press LNX/WIN: Alt+Ctrl+F2 - MAC Cmd+F2, Pulsar will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Pulsar will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
Pulsar also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using LNX/WIN: Ctrl+Shift+Q - MAC: Alt+Cmd+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
',3),Dt=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Shift+K"),t(" - Delete current line")]),e("li",null,[e("kbd",null,"Ctrl+Backspace"),t(" - Delete to beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Delete"),t(" - Delete to end of word")])],-1),jt=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Shift+K"),t(" - Delete current line")]),e("li",null,[e("kbd",null,"Alt+Backspace"),t(" or "),e("kbd",null,"Alt+H"),t(" - Delete to beginning of word")]),e("li",null,[e("kbd",null,"Alt+Delete"),t(" or "),e("kbd",null,"Alt+D"),t(" - Delete to end of word")]),e("li",null,[e("kbd",null,"Cmd+Delete"),t(" - Delete to end of line")]),e("li",null,[e("kbd",null,"Ctrl+K"),t(" - Cut to end of line")]),e("li",null,[e("kbd",null,"Cmd+Backspace"),t(" - Delete to beginning of line")])],-1),Yt=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Shift+K"),t(" - Delete current line")]),e("li",null,[e("kbd",null,"Ctrl+Backspace"),t(" - Delete to beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Delete"),t(" - Delete to end of word")])],-1),Et=e("h3",{id:"multiple-cursors-and-selections",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#multiple-cursors-and-selections","aria-hidden":"true"},"#"),t(" Multiple Cursors and Selections")],-1),zt=e("p",null,"One of the cool things that Pulsar can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.",-1),Ut=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Click"),t(" - Add a new cursor at the clicked location")]),e("li",null,[e("kbd",null,"Alt+Shift+Up/Down"),t(" - Add another cursor above/below the current cursor")]),e("li",null,[e("kbd",null,"Ctrl+D"),t(" - Select the next word in the document that is the same as the currently selected word")]),e("li",null,[e("kbd",null,"Alt+F3"),t(" - Select all words in the document that are the same as the currently selected word")])],-1),$t=e("ul",null,[e("li",null,[e("kbd",null,"Cmd+Click"),t(" - Add a new cursor at the clicked location")]),e("li",null,[e("kbd",null,"Ctrl+Shift+Up/Down"),t(" - Add another cursor above/below the current cursor")]),e("li",null,[e("kbd",null,"Cmd+D"),t(" - Select the next word in the document that is the same as the currently selected word")]),e("li",null,[e("kbd",null,"Cmd+Ctrl+G"),t(" - Select all words in the document that are the same as the currently selected word")]),e("li",null,[e("kbd",null,"Cmd+Shift+L"),t(" - Convert a multi-line selection into multiple cursors")])],-1),Bt=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Click"),t(" - Add a new cursor at the clicked location")]),e("li",null,[e("kbd",null,"Alt+Ctrl+Up/Down"),t(" - Add another cursor above/below the current cursor")]),e("li",null,[e("kbd",null,"Ctrl+D"),t(" - Select the next word in the document that is the same as the currently selected word")]),e("li",null,[e("kbd",null,"Alt+F3"),t(" - Select all words in the document that are the same as the currently selected word")])],-1),Vt=r('Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any package or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the LNX/WIN: Ctrl - MAC: Cmd key pressed down to select multiple regions of your text simultaneously.
Return to a single cursor with Esc or by clicking anywhere in the file to position a cursor there as normal.
Pulsar comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
',7),Xt={href:"https://github.com/pulsar-edit/whitespace",target:"_blank",rel:"noopener noreferrer"},Kt=e("code",null,"whitespace",-1),Jt=r('Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Pulsar, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Pulsar will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Pulsar ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Pulsar will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, \u201C\u201D
, \u2018\u2019
, \xAB\xBB
, \u2039\u203A
, and ``
when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Pulsar will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
',8),Zt=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+M"),t(" - Jump to the bracket matching the one adjacent to the cursor. It jumps to the nearest enclosing bracket when there's no adjacent bracket.")]),e("li",null,[e("kbd",null,"Alt+Ctrl+,"),t(" - Select all the text inside the current brackets")]),e("li",null,[e("kbd",null,"Alt+Ctrl+."),t(" - Close the current XML/HTML tag")])],-1),Qt=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+M"),t(" - Jump to the bracket matching the one adjacent to the cursor. It jumps to the nearest enclosing bracket when there's no adjacent bracket.")]),e("li",null,[e("kbd",null,"Cmd+Ctrl+M"),t(" - Select all the text inside the current brackets")]),e("li",null,[e("kbd",null,"Alt+Cmd+."),t(" - Close the current XML/HTML tag")])],-1),en=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+M"),t(" - Jump to the bracket matching the one adjacent to the cursor. It jumps to the nearest enclosing bracket when there's no adjacent bracket.")]),e("li",null,[e("kbd",null,"Alt+Ctrl+,"),t(" - Select all the text inside the current brackets")]),e("li",null,[e("kbd",null,"Alt+Ctrl+."),t(" - Close the current XML/HTML tag")])],-1),tn={href:"https://github.com/pulsar-edit/bracket-matcher",target:"_blank",rel:"noopener noreferrer"},nn=e("h3",{id:"encoding",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#encoding","aria-hidden":"true"},"#"),t(" Encoding")],-1),an=e("p",null,"Pulsar also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.",-1),on=e("p",null,[e("kbd",null,"Alt+U"),t(" - Toggle menu to change file encoding")],-1),sn=e("p",null,[e("kbd",null,"Ctrl+Shift+U"),t(" - Toggle menu to change file encoding")],-1),ln=e("p",null,[e("kbd",null,"Ctrl+Shift+U"),t(" - Toggle menu to change file encoding")],-1),rn=e("p",null,"If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.",-1),cn=e("p",null,"When you open a file, Pulsar will try to auto-detect the encoding. If Pulsar can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.",-1),dn=e("p",null,[e("img",{src:_,alt:"Changing your file encoding"})],-1),pn=e("p",null,"If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.",-1),un={href:"https://github.com/pulsar-edit/encoding-selector",target:"_blank",rel:"noopener noreferrer"},hn=e("h2",{id:"find-and-replace",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#find-and-replace","aria-hidden":"true"},"#"),t(" Find and Replace")],-1),mn=e("p",null,"Finding and replacing text in your file or project is quick and easy in Pulsar.",-1),gn=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+F"),t(" - Search within a buffer")]),e("li",null,[e("kbd",null,"Ctrl+Shift+F"),t(" - Search the entire project")])],-1),bn=e("ul",null,[e("li",null,[e("kbd",null,"Cmd+F"),t(" - Search within a buffer")]),e("li",null,[e("kbd",null,"Cmd+Shift+F"),t(" - Search the entire project")])],-1),fn=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+F"),t(" - Search within a buffer")]),e("li",null,[e("kbd",null,"Ctrl+Shift+F"),t(" - Search the entire project")])],-1),kn=r('If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press LNX/WIN: Cmd+F - MAC: Ctrl+F, type in a search string and press LNX/WIN/MAC: Enter or LNX/WINF3 - MAC: Cmd+G or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurrences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Atom" with the string "Pulsar", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
',4),yn={class:"custom-container note"},vn=e("p",{class:"custom-container-title"},"Note",-1),wn=e("p",null,[e("strong",null,"Note:"),t(" Pulsar uses JavaScript regular expressions to perform regular expression searches.")],-1),_n={href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions",target:"_blank",rel:"noopener noreferrer"},Cn=r('You can also find and replace throughout your entire project if you invoke the panel with LNX/WIN: Ctrl+Shift+F - MAC: Cmd+Shift+F.
This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.
',3),xn={href:"https://en.wikipedia.org/wiki/Glob_%28programming%29",target:"_blank",rel:"noopener noreferrer"},Sn=e("code",null,"src/*.js",-1),Pn=e("code",null,"src",-1),qn=e("code",null,"**",-1),Tn=e("code",null,"docs/**/*.md",-1),An=e("code",null,"docs/a/foo.md",-1),In=e("code",null,"docs/a/b/foo.md",-1),Ln=e("p",null,[t("When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders "),e("code",null,"/path1/folder1"),t(" and "),e("code",null,"/path2/folder2"),t(" open, you could enter a pattern starting with "),e("code",null,"folder1"),t(" to search only in the first folder.")],-1),Mn=e("p",null,[t("Press "),e("kbd",null,"Esc"),t(" while focused on the Find and Replace panel to clear the pane from your workspace.")],-1),Rn={href:"https://github.com/pulsar-edit/find-and-replace",target:"_blank",rel:"noopener noreferrer"},Nn={href:"https://github.com/pulsar-edit/scandal",target:"_blank",rel:"noopener noreferrer"},Fn=r(`Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Pulsar, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\\.pulsar
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Pulsar. You can also easily open up that file by selecting the LNX: Edit > Snippets - MAC: Pulsar > Snippets - WIN: File > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
Warning
Snippet keys, unlike CSS selectors, can only be repeated once per level. If there are duplicate keys at the same level, then only the last one will be read. See Configuring with CSON for more information.
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (\${1:true}) {
+ $2
+ } else if (\${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
\u{1F4A5} Just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Pulsar should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
Again, see Configuring with CSON for more information on CSON key structure and non-repeatability.
If you're still looking to save some typing time, Pulsar also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
',6),En={href:"https://github.com/pulsar-edit/autocomplete-plus",target:"_blank",rel:"noopener noreferrer"},zn=e("h2",{id:"folding",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#folding","aria-hidden":"true"},"#"),t(" Folding")],-1),Un=e("p",null,"If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.",-1),$n=e("p",null,[t("You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the "),e("kbd",null,"Alt+Ctrl+["),t(" and "),e("kbd",null,"Alt+Ctrl+]"),t(" keybindings.")],-1),Bn=e("p",null,[e("img",{src:p,alt:"Code folding example",title:"Code folding example"})],-1),Vn=e("p",null,[t("To fold everything, use "),e("kbd",null,"Alt+Ctrl+Shift+["),t(" and to unfold everything use "),e("kbd",null,"Alt+Ctrl+Shift+]"),t(". You can also fold at a specific indentation level with "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+0-9"),t(" where the number is the indentation depth.")],-1),Xn=e("p",null,[t("Finally, you can fold arbitrary sections of your code or text by making a selection and then typing "),e("kbd",null,"Alt+Ctrl+F"),t(' or choosing "Fold Selection" in the Command Palette.')],-1),Kn=e("p",null,[t("You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the "),e("kbd",null,"Alt+Cmd+["),t(" and "),e("kbd",null,"Alt+Cmd+]"),t(" keybindings.")],-1),Jn=e("p",null,[e("img",{src:p,alt:"Code folding example",title:"Code folding example"})],-1),Zn=e("p",null,[t("To fold everything, use "),e("kbd",null,"Alt+Cmd+Shift+["),t(" and to unfold everything use "),e("kbd",null,"Alt+Cmd+Shift+]"),t(". You can also fold at a specific indentation level with "),e("kbd",null,"Cmd+K"),t(),e("kbd",null,"Cmd+0-9"),t(" where the number is the indentation depth.")],-1),Qn=e("p",null,[t("Finally, you can fold arbitrary sections of your code or text by making a selection and then typing "),e("kbd",null,"Alt+Cmd+Ctrl+F"),t(' or choosing "Fold Selection" in the Command Palette.')],-1),ea=e("p",null,[t("You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the "),e("kbd",null,"Alt+Ctrl+["),t(" and "),e("kbd",null,"Alt+Ctrl+]"),t(" keybindings.")],-1),ta=e("p",null,[e("img",{src:p,alt:"Code folding example",title:"Code folding example"})],-1),na=e("p",null,[t("To fold everything, use "),e("kbd",null,"Alt+Ctrl+Shift+["),t(" and to unfold everything use "),e("kbd",null,"Alt+Ctrl+Shift+]"),t(". You can also fold at a specific indentation level with "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+0-9"),t(" where the number is the indentation depth.")],-1),aa=e("p",null,[t("Finally, you can fold arbitrary sections of your code or text by making a selection and then typing "),e("kbd",null,"Alt+Ctrl+F"),t(' or choosing "Fold Selection" in the Command Palette.')],-1),oa=e("h2",{id:"panes",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#panes","aria-hidden":"true"},"#"),t(" Panes")],-1),sa=e("p",null,[t("You can split any editor pane horizontally or vertically by using "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Up/Down/Left/Right"),t(" where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+Up/Down/Left/Right"),t(" where the direction is the direction the focus should move to.")],-1),ia=e("p",null,[e("img",{src:u,alt:"Multiple panes",title:"Multiple panes"})],-1),la=e("p",null,'Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.',-1),ra=e("p",null,[t("To close a pane, you can close all pane items with "),e("kbd",null,"Ctrl+W"),t('. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.')],-1),ca=e("p",null,[t("You can split any editor pane horizontally or vertically by using "),e("kbd",null,"Cmd+K"),t(),e("kbd",null,"Up/Down/Left/Right"),t(" where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with "),e("kbd",null,"Cmd+K"),t(),e("kbd",null,"Cmd+Up/Down/Left/Right"),t(" where the direction is the direction the focus should move to.")],-1),da=e("p",null,[e("img",{src:u,alt:"Multiple panes",title:"Multiple panes"})],-1),pa=e("p",null,'Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.',-1),ua=e("p",null,[t("To close a pane, you can close all pane items with "),e("kbd",null,"Cmd+W"),t('. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.')],-1),ha=e("p",null,[t("You can split any editor pane horizontally or vertically by using "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Up/Down/Left/Right"),t(" where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+Up/Down/Left/Right"),t(" where the direction is the direction the focus should move to.")],-1),ma=e("p",null,[e("img",{src:u,alt:"Multiple panes",title:"Multiple panes"})],-1),ga=e("p",null,'Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.',-1),ba=e("p",null,[t("To close a pane, you can close all pane items with "),e("kbd",null,"Ctrl+W"),t('. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.')],-1),fa={class:"custom-container tip"},ka=e("p",{class:"custom-container-title"},"Tip",-1),ya={href:"https://github.com/pulsar-edit/tabs",target:"_blank",rel:"noopener noreferrer"},va=r('"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
The "grammar" of a file is what language Pulsar has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in Snippets.
When you load a file, Pulsar does a little work to try to figure out what type of file it is. Largely this is accomplished by looking at its file extension (.md
is generally a Markdown file, etc.), though sometimes it has to inspect the content a bit to figure it out.
When you open a file and Pulsar can't determine a grammar for the file, it will default to "Plain Text", which is the simplest one. If it does default to "Plain Text", picks the wrong grammar for the file, or if for any reason you wish to change the selected grammar, you can pull up the Grammar Selector with Ctrl+Shift+L.
When the grammar of a file is changed, Atom will remember that for the current session.
',15),wa={href:"https://github.com/pulsar-edit/pulsar/tree/master/packages/grammar-selector",target:"_blank",rel:"noopener noreferrer"},_a=e("h2",{id:"version-control-in-pulsar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#version-control-in-pulsar","aria-hidden":"true"},"#"),t(" Version Control in Pulsar")],-1),Ca={href:"https://git-scm.com",target:"_blank",rel:"noopener noreferrer"},xa={href:"https://github.com",target:"_blank",rel:"noopener noreferrer"},Sa=r('In order to use version control in Pulsar, the project root needs to contain the Git repository.
The LNX/WIN: Alt+Ctrl+Z - MAC: Alt+Cmd+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use LNX/WIN: Ctrl+Z - MAC: Cmd+Z afterwards to restore the previous contents.
$ git config --global core.editor "pulsar --wait"
+
This package also adds Alt+G Down and Alt+GUp keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
The GitHub package brings Git and GitHub integration right inside Pulsar.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Pulsar and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the LNX/WIN: Cmd-Left - MAC: Ctrl-Left or LNX/WIN: Ctrl-Right - MAC: Cmd-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "\u{1F464}\u2795" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Pulsar will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting LNX/WIN: Ctrl+Shift+; - MAC: Cmd+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Pulsar will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Pulsar ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press LNX: Ctrl+Ins - WIN: Ctrl+C - MAC: Cmd+C or if you right-click in the preview pane and choose "Copy as HTML".
',6),lo={href:"https://github.com/pulsar-edit/markdown-preview",target:"_blank",rel:"noopener noreferrer"},ro=r(`There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing Snippets: Available
in the Command Palette.
Now that we are feeling comfortable with just about everything built into Pulsar, let's look at how to tweak it. Perhaps there is a keybinding that you use a lot but feels wrong or a color that isn't quite right for you. Pulsar is amazingly flexible, so let's go over some of the simpler flexes it can do.
key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Pulsar's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\\.pulsar
directory. You can open this file in an editor from the LNX: Edit > Stylesheet - MAC: Pulsar > Stylesheet - WIN: File > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Pulsar. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
',10),go={class:"custom-container tip"},bo=e("p",{class:"custom-container-title"},"Tip",-1),fo={href:"http://www.lesscss.org",target:"_blank",rel:"noopener noreferrer"},ko=e("p",null,[t("If you prefer to use CSS instead, you can do that in the same "),e("code",null,"styles.less"),t(" file, since CSS is also valid in Less.")],-1),yo=r(`Pulsar keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Pulsar keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Pulsar's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Pulsar is started. It will always be loaded last, giving you the chance to override bindings that are defined by Pulsar's core keymaps or third-party packages. You can open this file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Pulsar in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the LNX/WIN: Ctrl+. - MAC: Cmd+. key combination. It will show you what keys Pulsar saw you press and what command Pulsar executed because of that combination.
Pulsar loads configuration settings from the config.cson
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\\.pulsar
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the LNX: Edit > Config - MAC: Pulsar > Config - WIN: File > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of PulsarprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Pulsar will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \\n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Pulsar to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Pulsar to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Pulsar are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Pulsar Home directory will, by default, be called .pulsar
and will be located in the root of the home directory of the user.
An environment variable can be used to make Pulsar use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Pulsar Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Pulsar settings.
In addition to using the ATOM_HOME
environment variable, Pulsar can also be set to use "Portable Mode".
Portable Mode is most useful for taking Pulsar with you, with all your custom settings and packages, from machine to machine. This may take the form of keeping Pulsar on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Pulsar is in Portable Mode when there is a directory named .pulsar sibling to the directory in which the pulsar executable file lives. For example, the installed Pulsar directory can be placed into a Dropbox folder next to a .pulsar folder.
With such a setup, Pulsar will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Pulsar provides a command-line parameter option for setting Portable Mode.
$ pulsar --portable
+
Executing pulsar
with the --portable
option will take the .pulsar
directory you have in the default location (~/.pulsar
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
At this point you should be something of a Pulsar master user. You should be able to navigate and manipulate your text and files like a wizard. You should also be able to customize Pulsar backwards and forwards to make it look and act just how you want it to.
In the next section, we're going to kick it up a notch: we'll take a look at changing and adding new functionality to the core of Pulsar itself. We're going to start creating packages for Pulsar. If you can dream it, you can build it.
`,53);function vo(wo,_o){const a=d("ExternalLinkIcon"),m=d("RouterLink"),c=d("Tabs");return ke(),ye("div",null,[we,e("p",null,[t("First we'll start with the Pulsar package system. As we mentioned previously, Pulsar itself is a very basic core of functionality that ships with a number of useful packages that add new features like the "),e("a",_e,[t("Tree View"),n(a)]),t(" and the "),e("a",Ce,[t("Settings View"),n(a)]),t(".")]),e("p",null,[t("In fact, there are more than 80 packages that comprise all of the functionality that is available in Pulsar by default. For example, the "),e("a",xe,[t("Welcome screen"),n(a)]),t(" that you see when you first start Pulsar, the "),e("a",Se,[t("spell checker"),n(a)]),t(", the "),e("a",Pe,[t("themes"),n(a)]),t(" and the "),e("a",qe,[t("Fuzzy Finder"),n(a)]),t(" are all packages that are separately maintained and all use the same APIs that you have access to, as we'll see in great detail in "),n(m,{to:"/docs/launch-manual/sections/core-hacking/"},{default:o(()=>[t("Hacking the Core")]),_:1}),t(".")]),Te,Ae,e("p",null,[t("The packages listed here have been published to "),e("a",Ie,[t("https://web.pulsar-edit.dev"),n(a)]),h("TODO:Change address to final URL (if this is not it)"),t(" which is the official registry for Pulsar packages. Searching on the Settings View will go to the Pulsar package registry and pull in anything that matches your search terms.")]),Le,e("p",null,[t("For example "),Me,t(" installs the "),Re,t(" release of the "),e("a",Ne,[t("minimap"),n(a)]),t(" package.")]),Fe,e("p",null,[t("By default ppm will be using the "),e("a",Oe,[t("Pulsar Package Repository"),n(a)]),t(". However you can also use it to install from other locations which can be useful if you are trying to install a package not published to the Pulsar Package Repository.")]),We,e("p",null,[t("For example to install the "),e("a",He,[t("Generic-LSP"),n(a)]),t(" package from GitHub you could use either:")]),Ge,n(c,{id:"154",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[De,je,Ye]),tab1:o(({title:s,value:i,isActive:l})=>[Ee,ze,Ue]),tab2:o(({title:s,value:i,isActive:l})=>[$e,Be,Ve]),_:1}),Xe,n(c,{id:"288",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar "},{tab0:o(({title:s,value:i,isActive:l})=>[Ke]),tab1:o(({title:s,value:i,isActive:l})=>[Je]),tab2:o(({title:s,value:i,isActive:l})=>[Ze]),_:1}),Qe,n(c,{id:"305",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[et]),tab1:o(({title:s,value:i,isActive:l})=>[tt]),tab2:o(({title:s,value:i,isActive:l})=>[nt]),_:1}),at,e("p",null,[t("You can generate a "),ot,t(" file by using the "),e("a",st,[t("ctags utility"),n(a)]),t(". Once it is installed, you can use it to generate a "),it,t(" file by running a command to generate it. See the "),e("a",lt,[t("ctags documentation"),n(a)]),t(" for details.")]),n(c,{id:"328",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[rt]),tab1:o(({title:s,value:i,isActive:l})=>[ct]),tab2:o(({title:s,value:i,isActive:l})=>[dt]),_:1}),e("p",null,[t("You can customize how tags are generated by creating your own "),pt,t(" file in your home directory, "),ut,t(": "),ht,t(" - "),mt,t(": "),gt,t(". An example can be found "),e("a",bt,[t("here"),n(a)]),t(".")]),e("p",null,[t("The symbols navigation functionality is implemented in the "),e("a",ft,[t("symbols-view"),n(a)]),t(" package.")]),kt,e("p",null,[t("The bookmarks functionality is implemented in the "),e("a",yt,[t("bookmarks"),n(a)]),t(" package.")]),vt,wt,_t,n(c,{id:"383",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[Ct,xt,St]),tab1:o(({title:s,value:i,isActive:l})=>[Pt,qt,Tt]),tab2:o(({title:s,value:i,isActive:l})=>[At,It,Lt]),_:1}),Mt,Rt,Nt,Ft,n(c,{id:"611",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[Ot]),tab1:o(({title:s,value:i,isActive:l})=>[Wt]),tab2:o(({title:s,value:i,isActive:l})=>[Ht]),_:1}),Gt,n(c,{id:"714",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[Dt]),tab1:o(({title:s,value:i,isActive:l})=>[jt]),tab2:o(({title:s,value:i,isActive:l})=>[Yt]),_:1}),Et,zt,n(c,{id:"794",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[Ut]),tab1:o(({title:s,value:i,isActive:l})=>[$t]),tab2:o(({title:s,value:i,isActive:l})=>[Bt]),_:1}),Vt,e("p",null,[t("The whitespace commands are implemented in the "),e("a",Xt,[t("whitespace"),n(a)]),t(" package. The settings for the whitespace commands are managed on the page for the "),Kt,t(" package.")]),Jt,n(c,{id:"923",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[Zt]),tab1:o(({title:s,value:i,isActive:l})=>[Qt]),tab2:o(({title:s,value:i,isActive:l})=>[en]),_:1}),e("p",null,[t("The brackets functionality is implemented in the "),e("a",tn,[t("bracket-matcher"),n(a)]),t(" package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.")]),nn,an,n(c,{id:"991",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[on]),tab1:o(({title:s,value:i,isActive:l})=>[sn]),tab2:o(({title:s,value:i,isActive:l})=>[ln]),_:1}),rn,cn,dn,pn,e("p",null,[t("The encoding selector is implemented in the "),e("a",un,[t("encoding-selector"),n(a)]),t(" package.")]),hn,mn,n(c,{id:"1031",data:[{title:"Linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[gn]),tab1:o(({title:s,value:i,isActive:l})=>[bn]),tab2:o(({title:s,value:i,isActive:l})=>[fn]),_:1}),kn,e("div",yn,[vn,wn,e("p",null,[t("When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, \u2026 $&. Refer to JavaScript's "),e("a",_n,[t("guide to regular expressions"),n(a)]),t(" to learn more about regular expression syntax you can use in Pulsar.")])]),Cn,e("p",null,[t("You can limit a search to a subset of the files in your project by entering a "),e("a",xn,[t("glob pattern"),n(a)]),t(' into the "File/Directory pattern" text box. For example, the pattern '),Sn,t(" would restrict the search to JavaScript files in the "),Pn,t(' directory. The "globstar" pattern ('),qn,t(") can be used to match arbitrarily many subdirectories. For example, "),Tn,t(" will match "),An,t(", "),In,t(", etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.")]),Ln,Mn,e("p",null,[t("The Find and Replace functionality is implemented in the "),e("a",Rn,[t("find-and-replace"),n(a)]),t(" package and uses the "),e("a",Nn,[t("scandal"),n(a)]),t(" Node module to do the actual searching.")]),Fn,e("p",null,[t("You can also use "),e("a",On,[t("CoffeeScript multi-line syntax"),n(a)]),t(" using "),Wn,t(" for larger templates:")]),Hn,e("p",null,[t("The snippets functionality is implemented in the "),e("a",Gn,[t("snippets"),n(a)]),t(" package.")]),e("p",null,[t("For more examples, see the snippets in the "),e("a",Dn,[t("language-html"),n(a)]),t(" and "),e("a",jn,[t("language-javascript"),n(a)]),t(" packages.")]),Yn,e("p",null,[t("The Autocomplete functionality is implemented in the "),e("a",En,[t("autocomplete-plus"),n(a)]),t(" package.")]),zn,Un,n(c,{id:"1256",data:[{title:"linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[$n,Bn,Vn,Xn]),tab1:o(({title:s,value:i,isActive:l})=>[Kn,Jn,Zn,Qn]),tab2:o(({title:s,value:i,isActive:l})=>[ea,ta,na,aa]),_:1}),oa,n(c,{id:"1305",data:[{title:"linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:o(({title:s,value:i,isActive:l})=>[sa,ia,la,ra]),tab1:o(({title:s,value:i,isActive:l})=>[ca,da,pa,ua]),tab2:o(({title:s,value:i,isActive:l})=>[ha,ma,ga,ba]),_:1}),e("div",fa,[ka,e("p",null,[t("If you don't like using tabs, you don't have to. You can disable the "),e("a",ya,[t("tabs package"),n(a)]),t(" and each pane will still support multiple pane items. You just won't have tabs to use to click between them.")])]),va,e("p",null,[t("The Grammar Selector functionality is implemented in the "),e("a",wa,[t("grammar-selector"),n(a)]),t(" package.")]),_a,e("p",null,[t("Version control is an important aspect of any project and Pulsar comes with basic "),e("a",Ca,[t("Git"),n(a)]),t(" and "),e("a",xa,[t("GitHub"),n(a)]),t(" integration built in.")]),Sa,e("p",null,[t("Pulsar ships with the "),e("a",Pa,[t("fuzzy-finder package"),n(a)]),t(" which provides "),qa,t(": "),Ta,t(" - "),Aa,t(": "),Ia,t(" to quickly open files in the project and "),La,t(": "),Ma,t(" - "),Ra,t(": "),Na,t(" to jump to any open editor. The package also provides "),Fa,t(": "),Oa,t(" - "),Wa,t(": "),Ha,t(" which displays a list of all the untracked and modified files in the project. These will be the same files that you would see on the command line if you ran "),Ga,t(".")]),Da,ja,Ya,e("p",null,[t("Pulsar can be used as your Git commit editor and ships with the "),e("a",Ea,[t("language-git package"),n(a)]),t(" which adds syntax highlighting to edited commit, merge, and rebase messages.")]),za,Ua,h("TODO: Check this still works in Pulsar"),$a,e("p",null,[t("The "),e("a",Ba,[t("language-git"),n(a)]),t(" package will help remind you to be brief by colorizing the first lines of commit messages when they're longer than 50 or 65 characters.")]),Va,e("p",null,[t("The "),e("a",Xa,[t("status-bar"),n(a)]),t(" package that ships with Pulsar includes several Git decorations that display on the right side of the status bar:")]),Ka,Ja,Za,e("p",null,[t("The included "),e("a",Qa,[t("git-diff"),n(a)]),t(" package colorizes the gutter next to lines that have been added, edited, or removed.")]),eo,e("p",null,[t("Learn more about "),e("a",to,[t("merge vs. rebase"),n(a)]),t(".")]),no,e("p",null,[t("Though it is probably most common to use Pulsar to write software code, Pulsar can also be used to write prose quite effectively. Most often this is done in some sort of markup language such as Asciidoc or "),e("a",ao,[t("Markdown"),n(a)]),t(" (in which this manual is written). Here we'll quickly cover a few of the tools Pulsar provides for helping you write prose.")]),oo,e("p",null,[t("The spell checking is implemented in the "),e("a",so,[t("spell-check"),n(a)]),t(" package.")]),io,e("p",null,[t("Markdown preview is implemented in the "),e("a",lo,[t("markdown-preview"),n(a)]),t(" package.")]),ro,e("p",null,[t("All of Pulsar's config files (with the exception of your "),co,t(" and your "),po,t(" are written in CSON, short for "),e("a",uo,[t("CoffeeScript Object Notation"),n(a)]),t(". Just like its namesake JSON, "),e("a",ho,[t("JavaScript Object Notation"),n(a)]),t(", CSON is a text format for storing structured data in the form of simple objects made up of key-value pairs.")]),mo,e("div",go,[bo,e("p",null,[t("If you are unfamiliar with Less, it is a basic CSS preprocessor that makes some things in CSS a bit easier. You can learn more about it at "),e("a",fo,[t("lesscss.org"),n(a)]),t(".")]),ko]),yo])}const Go=fe(ve,[["render",vo],["__file","index.html.vue"]]);export{Go as default}; diff --git a/assets/index.html.bed1e49d.js b/assets/index.html.bed1e49d.js new file mode 100644 index 0000000000..980804fd15 --- /dev/null +++ b/assets/index.html.bed1e49d.js @@ -0,0 +1 @@ +import{_ as s,o as c,c as l,a,d as t,w as n,b as e,f as d,r as i}from"./app.0e1565ce.js";const h={},u=d('This aims to be a collection of information about the packages published for the Pulsar Editor. Currently, anything in Core Packages is an archive of every Wiki that was published from the upstream Atom Organization.
This means some of this information may be old, or no longer relevant. If it is, it has been marked as so at the top of the file under a warning. None-the-less the information can still be helpful, and may prove to be valuable. Which is why it has been archived here.
As well, we have a record of community packages as well. This is meant to be a record of these. This section should be updated as we get more community contributions of these. The process for adding this will be added at a later date.
A collection of miscellaneous resources around Pulsar and the project.
A list of terms and their meanings used by the application and community.
The Pulsar API reference.
A list of tools and services used by the Pulsar team and information about them.
',7),t=[i];function n(l,c){return e(),o("div",null,t)}const h=a(r,[["render",n],["__file","index.html.vue"]]);export{h as default}; diff --git a/assets/index.html.d36c0486.js b/assets/index.html.d36c0486.js new file mode 100644 index 0000000000..aee64951f7 --- /dev/null +++ b/assets/index.html.d36c0486.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6c28ad56","path":"/tag/release/","title":"release Tag","lang":"en-US","frontmatter":{"title":"release Tag","blog":{"type":"category","name":"release","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.d51edc97.js b/assets/index.html.d51edc97.js new file mode 100644 index 0000000000..4a1b99f402 --- /dev/null +++ b/assets/index.html.d51edc97.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-4368211c","path":"/docs/atom-archive/atom-server-side-apis/","title":"Appendix E : Atom server-side APIs","lang":"en-US","frontmatter":{"title":"Appendix E : Atom server-side APIs","sitemap":{"priority":0.1}},"excerpt":"","headers":[{"level":2,"title":"Atom server-side APIs","slug":"atom-server-side-apis","link":"#atom-server-side-apis","children":[{"level":3,"title":"Atom package server API","slug":"atom-package-server-api","link":"#atom-package-server-api","children":[]},{"level":3,"title":"Atom update server API","slug":"atom-update-server-api","link":"#atom-update-server-api","children":[]}]}],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":5},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":0.34,"words":102},"filePathRelative":"docs/atom-archive/atom-server-side-apis/index.md"}');export{e as data}; diff --git a/assets/index.html.d872f358.js b/assets/index.html.d872f358.js new file mode 100644 index 0000000000..f3635b121e --- /dev/null +++ b/assets/index.html.d872f358.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-278e8fc5","path":"/docs/launch-manual/sections/core-hacking/","title":"Hacking the Core","lang":"en-US","frontmatter":{"lang":"en-US","title":"Hacking the Core","description":"Info on building from source + hacking on Pulsar's core"},"excerpt":"","headers":[{"level":2,"title":"Building Pulsar","slug":"building-pulsar","link":"#building-pulsar","children":[{"level":3,"title":"Requirements and dependencies","slug":"requirements-and-dependencies","link":"#requirements-and-dependencies","children":[]},{"level":3,"title":"Building and running the application","slug":"building-and-running-the-application","link":"#building-and-running-the-application","children":[]},{"level":3,"title":"Building binaries","slug":"building-binaries","link":"#building-binaries","children":[]}]},{"level":2,"title":"Using ppm (Pulsar Package Manager)","slug":"using-ppm-pulsar-package-manager","link":"#using-ppm-pulsar-package-manager","children":[]},{"level":2,"title":"Hacking on the Core","slug":"hacking-on-the-core","link":"#hacking-on-the-core","children":[{"level":3,"title":"Running in Development Mode","slug":"running-in-development-mode","link":"#running-in-development-mode","children":[]},{"level":3,"title":"Running Pulsar Core Tests Locally","slug":"running-pulsar-core-tests-locally","link":"#running-pulsar-core-tests-locally","children":[]}]},{"level":2,"title":"Tools of the Trade","slug":"tools-of-the-trade","link":"#tools-of-the-trade","children":[]},{"level":2,"title":"The Init File","slug":"the-init-file","link":"#the-init-file","children":[]},{"level":2,"title":"Package: Word Count","slug":"package-word-count","link":"#package-word-count","children":[{"level":3,"title":"Package Generator","slug":"package-generator","link":"#package-generator","children":[]},{"level":3,"title":"Developing Our Package","slug":"developing-our-package","link":"#developing-our-package","children":[]},{"level":3,"title":"Basic Debugging","slug":"basic-debugging","link":"#basic-debugging","children":[]},{"level":3,"title":"Testing","slug":"testing","link":"#testing","children":[]},{"level":3,"title":"Summary","slug":"summary","link":"#summary","children":[]}]},{"level":2,"title":"Package: Modifying Text","slug":"package-modifying-text","link":"#package-modifying-text","children":[{"level":3,"title":"Basic Text Insertion","slug":"basic-text-insertion","link":"#basic-text-insertion","children":[]},{"level":3,"title":"Add the ASCII Art","slug":"add-the-ascii-art","link":"#add-the-ascii-art","children":[]},{"level":3,"title":"Summary","slug":"summary-1","link":"#summary-1","children":[]}]},{"level":2,"title":"Package: Active Editor Info","slug":"package-active-editor-info","link":"#package-active-editor-info","children":[{"level":3,"title":"Create the Package","slug":"create-the-package","link":"#create-the-package","children":[]},{"level":3,"title":"Add an Opener","slug":"add-an-opener","link":"#add-an-opener","children":[]},{"level":3,"title":"Updating the View","slug":"updating-the-view","link":"#updating-the-view","children":[]},{"level":3,"title":"Constraining Our Item's Locations","slug":"constraining-our-item-s-locations","link":"#constraining-our-item-s-locations","children":[]},{"level":3,"title":"Show Active Editor Info","slug":"show-active-editor-info","link":"#show-active-editor-info","children":[]},{"level":3,"title":"Serialization","slug":"serialization","link":"#serialization","children":[]},{"level":3,"title":"Summary","slug":"summary-2","link":"#summary-2","children":[]}]},{"level":2,"title":"Creating a Theme","slug":"creating-a-theme","link":"#creating-a-theme","children":[{"level":3,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[]},{"level":3,"title":"Creating a Syntax Theme","slug":"creating-a-syntax-theme","link":"#creating-a-syntax-theme","children":[]},{"level":3,"title":"Creating a UI Theme","slug":"creating-a-ui-theme","link":"#creating-a-ui-theme","children":[]},{"level":3,"title":"Theme Variables","slug":"theme-variables","link":"#theme-variables","children":[]},{"level":3,"title":"Development workflow","slug":"development-workflow","link":"#development-workflow","children":[]},{"level":3,"title":"Publish your theme","slug":"publish-your-theme","link":"#publish-your-theme","children":[]}]},{"level":2,"title":"Creating a Grammar","slug":"creating-a-grammar","link":"#creating-a-grammar","children":[{"level":3,"title":"Getting Started","slug":"getting-started-1","link":"#getting-started-1","children":[]},{"level":3,"title":"The Parser","slug":"the-parser","link":"#the-parser","children":[]},{"level":3,"title":"The Package","slug":"the-package","link":"#the-package","children":[]},{"level":3,"title":"The Grammar File","slug":"the-grammar-file","link":"#the-grammar-file","children":[]},{"level":3,"title":"Basic Fields","slug":"basic-fields","link":"#basic-fields","children":[]},{"level":3,"title":"Language Recognition","slug":"language-recognition","link":"#language-recognition","children":[]},{"level":3,"title":"Syntax Highlighting","slug":"syntax-highlighting","link":"#syntax-highlighting","children":[]},{"level":3,"title":"Language Injection","slug":"language-injection","link":"#language-injection","children":[]},{"level":3,"title":"Code Folding","slug":"code-folding","link":"#code-folding","children":[]},{"level":3,"title":"Comments","slug":"comments","link":"#comments","children":[]},{"level":3,"title":"Example Packages","slug":"example-packages","link":"#example-packages","children":[]}]},{"level":2,"title":"Creating a Legacy TextMate Grammar","slug":"creating-a-legacy-textmate-grammar","link":"#creating-a-legacy-textmate-grammar","children":[{"level":3,"title":"Getting Started","slug":"getting-started-2","link":"#getting-started-2","children":[]},{"level":3,"title":"Create the Package","slug":"create-the-package-1","link":"#create-the-package-1","children":[]},{"level":3,"title":"Adding Patterns","slug":"adding-patterns","link":"#adding-patterns","children":[]},{"level":3,"title":"Begin/End Patterns","slug":"begin-end-patterns","link":"#begin-end-patterns","children":[]},{"level":3,"title":"Repositories and the Include keyword, or how to avoid duplication","slug":"repositories-and-the-include-keyword-or-how-to-avoid-duplication","link":"#repositories-and-the-include-keyword-or-how-to-avoid-duplication","children":[]},{"level":3,"title":"Where to Go from Here","slug":"where-to-go-from-here","link":"#where-to-go-from-here","children":[]}]},{"level":2,"title":"Converting from TextMate","slug":"converting-from-textmate","link":"#converting-from-textmate","children":[{"level":3,"title":"Converting a TextMate Grammar Bundle","slug":"converting-a-textmate-grammar-bundle","link":"#converting-a-textmate-grammar-bundle","children":[]},{"level":3,"title":"Converting a TextMate Syntax Theme","slug":"converting-a-textmate-syntax-theme","link":"#converting-a-textmate-syntax-theme","children":[]}]},{"level":2,"title":"Publishing","slug":"publishing","link":"#publishing","children":[{"level":3,"title":"Prepare Your Package","slug":"prepare-your-package","link":"#prepare-your-package","children":[]},{"level":3,"title":"Publish Your Package","slug":"publish-your-package","link":"#publish-your-package","children":[]}]},{"level":2,"title":"Iconography","slug":"iconography","link":"#iconography","children":[{"level":3,"title":"Overview","slug":"overview","link":"#overview","children":[]},{"level":3,"title":"Usage","slug":"usage","link":"#usage","children":[]},{"level":3,"title":"Size","slug":"size","link":"#size","children":[]},{"level":3,"title":"Usability","slug":"usability","link":"#usability","children":[]}]},{"level":2,"title":"Debugging","slug":"debugging","link":"#debugging","children":[{"level":3,"title":"Update to the Latest Version","slug":"update-to-the-latest-version","link":"#update-to-the-latest-version","children":[]},{"level":3,"title":"Using Safe Mode","slug":"using-safe-mode","link":"#using-safe-mode","children":[]},{"level":3,"title":"Clearing Saved State","slug":"clearing-saved-state","link":"#clearing-saved-state","children":[]},{"level":3,"title":"Reset to Factory Defaults","slug":"reset-to-factory-defaults","link":"#reset-to-factory-defaults","children":[]},{"level":3,"title":"Check for Linked Packages","slug":"check-for-linked-packages","link":"#check-for-linked-packages","children":[]},{"level":3,"title":"Check for Incompatible Packages","slug":"check-for-incompatible-packages","link":"#check-for-incompatible-packages","children":[]},{"level":3,"title":"Check Pulsar and Package Settings","slug":"check-pulsar-and-package-settings","link":"#check-pulsar-and-package-settings","children":[]},{"level":3,"title":"Check Your Configuration","slug":"check-your-configuration","link":"#check-your-configuration","children":[]},{"level":3,"title":"Check Your Keybindings","slug":"check-your-keybindings","link":"#check-your-keybindings","children":[]},{"level":3,"title":"Check Font Rendering Issues","slug":"check-font-rendering-issues","link":"#check-font-rendering-issues","children":[]},{"level":3,"title":"Check for Errors in the Developer Tools","slug":"check-for-errors-in-the-developer-tools","link":"#check-for-errors-in-the-developer-tools","children":[]},{"level":3,"title":"Find Crash Logs","slug":"find-crash-logs","link":"#find-crash-logs","children":[]},{"level":3,"title":"Diagnose Startup Performance","slug":"diagnose-startup-performance","link":"#diagnose-startup-performance","children":[]},{"level":3,"title":"Diagnose Runtime Performance","slug":"diagnose-runtime-performance","link":"#diagnose-runtime-performance","children":[]},{"level":3,"title":"Profiling Startup Performance","slug":"profiling-startup-performance","link":"#profiling-startup-performance","children":[]},{"level":3,"title":"Check Your Build Tools","slug":"check-your-build-tools","link":"#check-your-build-tools","children":[]},{"level":3,"title":"Check if your GPU is causing the problem","slug":"check-if-your-gpu-is-causing-the-problem","link":"#check-if-your-gpu-is-causing-the-problem","children":[]}]},{"level":2,"title":"Writing Specs","slug":"writing-specs","link":"#writing-specs","children":[{"level":3,"title":"Create a New Spec","slug":"create-a-new-spec","link":"#create-a-new-spec","children":[]},{"level":3,"title":"Asynchronous Specs","slug":"asynchronous-specs","link":"#asynchronous-specs","children":[]},{"level":3,"title":"Running Specs","slug":"running-specs","link":"#running-specs","children":[]},{"level":3,"title":"Customizing your test runner","slug":"customizing-your-test-runner","link":"#customizing-your-test-runner","children":[]}]},{"level":2,"title":"Handling URIs","slug":"handling-uris","link":"#handling-uris","children":[{"level":3,"title":"Modifying your package.json","slug":"modifying-your-package-json","link":"#modifying-your-package-json","children":[]},{"level":3,"title":"Modifying your Main Module","slug":"modifying-your-main-module","link":"#modifying-your-main-module","children":[]},{"level":3,"title":"Controlling Activation Deferral","slug":"controlling-activation-deferral","link":"#controlling-activation-deferral","children":[]},{"level":3,"title":"Linux Support","slug":"linux-support","link":"#linux-support","children":[]}]},{"level":2,"title":"Core URIs","slug":"core-uris","link":"#core-uris","children":[{"level":3,"title":"Cross-Platform Compatibility","slug":"cross-platform-compatibility","link":"#cross-platform-compatibility","children":[]}]},{"level":2,"title":"Contributing to Official Pulsar Packages","slug":"contributing-to-official-pulsar-packages","link":"#contributing-to-official-pulsar-packages","children":[{"level":3,"title":"Hacking on Packages","slug":"hacking-on-packages","link":"#hacking-on-packages","children":[]}]},{"level":2,"title":"Creating a Fork of a Core Package","slug":"creating-a-fork-of-a-core-package","link":"#creating-a-fork-of-a-core-package","children":[{"level":3,"title":"Creating Your New Package","slug":"creating-your-new-package","link":"#creating-your-new-package","children":[]},{"level":3,"title":"Merging Upstream Changes into Your Package","slug":"merging-upstream-changes-into-your-package","link":"#merging-upstream-changes-into-your-package","children":[]}]},{"level":2,"title":"Maintaining a Fork of a Core Package","slug":"maintaining-a-fork-of-a-core-package","link":"#maintaining-a-fork-of-a-core-package","children":[{"level":3,"title":"Step-by-step guide","slug":"step-by-step-guide","link":"#step-by-step-guide","children":[]}]},{"level":2,"title":"Summary","slug":"summary-3","link":"#summary-3","children":[]},{"level":2,"title":"Having trouble?","slug":"having-trouble","link":"#having-trouble","children":[]}],"git":{"updatedTime":1694141555000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":1.73,"words":520},"filePathRelative":"docs/launch-manual/sections/core-hacking/index.md"}`);export{e as data}; diff --git a/assets/index.html.d8f16f60.js b/assets/index.html.d8f16f60.js new file mode 100644 index 0000000000..ab7749c9c1 --- /dev/null +++ b/assets/index.html.d8f16f60.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-9c858a88","path":"/tag/feedback/","title":"feedback Tag","lang":"en-US","frontmatter":{"title":"feedback Tag","blog":{"type":"category","name":"feedback","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.d9369373.js b/assets/index.html.d9369373.js new file mode 100644 index 0000000000..7c4b92a4d3 --- /dev/null +++ b/assets/index.html.d9369373.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-181c8802","path":"/tag/electron/","title":"electron Tag","lang":"en-US","frontmatter":{"title":"electron Tag","blog":{"type":"category","name":"electron","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.db4b9cf5.js b/assets/index.html.db4b9cf5.js new file mode 100644 index 0000000000..3e7a25d31d --- /dev/null +++ b/assets/index.html.db4b9cf5.js @@ -0,0 +1 @@ +const i=JSON.parse('{"key":"v-a73e70e8","path":"/docs/resources/privacy/","title":"Privacy Policy","lang":"en-US","frontmatter":{"title":"Privacy Policy"},"excerpt":"","headers":[{"level":2,"title":"Pulsar Backend (https://api.pulsar-edit.dev)","slug":"pulsar-backend-https-api-pulsar-edit-dev","link":"#pulsar-backend-https-api-pulsar-edit-dev","children":[{"level":3,"title":"What Information is Collected","slug":"what-information-is-collected","link":"#what-information-is-collected","children":[]},{"level":3,"title":"How is this Information Collected","slug":"how-is-this-information-collected","link":"#how-is-this-information-collected","children":[]},{"level":3,"title":"What is Done with this Information","slug":"what-is-done-with-this-information","link":"#what-is-done-with-this-information","children":[]},{"level":3,"title":"Who has Access to this Information","slug":"who-has-access-to-this-information","link":"#who-has-access-to-this-information","children":[]},{"level":3,"title":"How Long is this information Kept","slug":"how-long-is-this-information-kept","link":"#how-long-is-this-information-kept","children":[]},{"level":3,"title":"TLDR","slug":"tldr","link":"#tldr","children":[]}]},{"level":2,"title":"Pulsar User Account","slug":"pulsar-user-account","link":"#pulsar-user-account","children":[{"level":3,"title":"What Information is Collected","slug":"what-information-is-collected-1","link":"#what-information-is-collected-1","children":[]},{"level":3,"title":"How is this Information Collected","slug":"how-is-this-information-collected-1","link":"#how-is-this-information-collected-1","children":[]},{"level":3,"title":"What is Done with this Information","slug":"what-is-done-with-this-information-1","link":"#what-is-done-with-this-information-1","children":[]},{"level":3,"title":"Who has Access to this Information","slug":"who-has-access-to-this-information-1","link":"#who-has-access-to-this-information-1","children":[]},{"level":3,"title":"How Long is this Information Kept","slug":"how-long-is-this-information-kept-1","link":"#how-long-is-this-information-kept-1","children":[]},{"level":3,"title":"How do I have this Information Deleted","slug":"how-do-i-have-this-information-deleted","link":"#how-do-i-have-this-information-deleted","children":[]},{"level":3,"title":"TLDR","slug":"tldr-1","link":"#tldr-1","children":[]}]},{"level":2,"title":"Pulsar Website (https://pulsar-edit.dev)","slug":"pulsar-website-https-pulsar-edit-dev","link":"#pulsar-website-https-pulsar-edit-dev","children":[{"level":3,"title":"TLDR","slug":"tldr-2","link":"#tldr-2","children":[]}]},{"level":2,"title":"Pulsar Editor","slug":"pulsar-editor","link":"#pulsar-editor","children":[{"level":3,"title":"TLDR","slug":"tldr-3","link":"#tldr-3","children":[]}]}],"git":{"updatedTime":1671043464000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.08,"words":25},"filePathRelative":"docs/resources/privacy/index.md"}');export{i as data}; diff --git a/assets/index.html.dcdc22a2.js b/assets/index.html.dcdc22a2.js new file mode 100644 index 0000000000..7363ef7d0c --- /dev/null +++ b/assets/index.html.dcdc22a2.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-01560935","path":"/timeline/","title":"Timeline","lang":"en-US","frontmatter":{"title":"Timeline","blog":{"type":"type","key":"timeline"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.de5ae0cd.js b/assets/index.html.de5ae0cd.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.de5ae0cd.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.e11796f0.js b/assets/index.html.e11796f0.js new file mode 100644 index 0000000000..e8615ec09b --- /dev/null +++ b/assets/index.html.e11796f0.js @@ -0,0 +1 @@ +import{_ as r,o as n,c as l,a as t,b as e,d as a,r as s}from"./app.0e1565ce.js";const c={},i=t("h1",{id:"autocomplete-plus",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#autocomplete-plus","aria-hidden":"true"},"#"),e(" Autocomplete-Plus")],-1),u=t("p",null,[e("Welcome to the "),t("code",null,"autocomplete-plus"),e(" wiki!")],-1),p={href:"https://github.com/atom/autocomplete-plus/wiki",target:"_blank",rel:"noopener noreferrer"},h={href:"https://github.com/pulsar-edit/autocomplete-plus",target:"_blank",rel:"noopener noreferrer"};function m(_,d){const o=s("ExternalLinkIcon");return n(),l("div",null,[i,u,t("p",null,[e("This wiki has been nearly directly taken from the taken from the "),t("a",p,[e("upstream Atom Wiki"),a(o)]),e(". That may mean that parts of it are out of date, but otherwise hopefully is a helpful resource relating to our "),t("a",h,[e("Autocomplete-Plus Repo"),a(o)]),e(".")])])}const k=r(c,[["render",m],["__file","index.html.vue"]]);export{k as default}; diff --git a/assets/index.html.e54b2982.js b/assets/index.html.e54b2982.js new file mode 100644 index 0000000000..2cb1c4e503 --- /dev/null +++ b/assets/index.html.e54b2982.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-ed2312e4","path":"/docs/atom-archive/","title":"Atom Documentation Archive","lang":"en-us","frontmatter":{"lang":"en-us","title":"Atom Documentation Archive","description":"Archived Atom Documentation in its original form","sitemap":{"priority":0.2}},"excerpt":"","headers":[{"level":2,"title":"Chapters","slug":"chapters","link":"#chapters","children":[]}],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":4},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":2},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Joshua MIller","email":"16845458+kaosine@users.noreply.github.com","commits":1},{"name":"softcode","email":"108250871+softcode589@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.7,"words":210},"filePathRelative":"docs/atom-archive/index.md"}');export{e as data}; diff --git a/assets/index.html.e66450b6.js b/assets/index.html.e66450b6.js new file mode 100644 index 0000000000..e5da6b0d1d --- /dev/null +++ b/assets/index.html.e66450b6.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-ad7b6e3a","path":"/docs/atom-archive/api/","title":"Reference : API","lang":"en-US","frontmatter":{"title":"Reference : API","sitemap":{"priority":0.1}},"excerpt":"","headers":[],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":0.02,"words":6},"filePathRelative":"docs/atom-archive/api/index.md"}');export{e as data}; diff --git a/assets/index.html.e66a9e6f.js b/assets/index.html.e66a9e6f.js new file mode 100644 index 0000000000..1fdad0b8b5 --- /dev/null +++ b/assets/index.html.e66a9e6f.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-744d024e","path":"/tag/","title":"Tag","lang":"en-US","frontmatter":{"title":"Tag","blog":{"type":"category","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.e9497bdd.js b/assets/index.html.e9497bdd.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.e9497bdd.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.ebeb7c37.js b/assets/index.html.ebeb7c37.js new file mode 100644 index 0000000000..05001df7ec --- /dev/null +++ b/assets/index.html.ebeb7c37.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-ad775132","path":"/docs/atom-archive/faq/","title":"Appendix B : FAQ","lang":"en-US","frontmatter":{"title":"Appendix B : FAQ","sitemap":{"priority":0.1}},"excerpt":"","headers":[{"level":2,"title":"FAQ","slug":"faq","link":"#faq","children":[{"level":3,"title":"Is Atom open source?","slug":"is-atom-open-source","link":"#is-atom-open-source","children":[]},{"level":3,"title":"What does Atom cost?","slug":"what-does-atom-cost","link":"#what-does-atom-cost","children":[]},{"level":3,"title":"What platforms does Atom run on?","slug":"what-platforms-does-atom-run-on","link":"#what-platforms-does-atom-run-on","children":[]},{"level":3,"title":"How can I contribute to Atom?","slug":"how-can-i-contribute-to-atom","link":"#how-can-i-contribute-to-atom","children":[]},{"level":3,"title":"Why does Atom collect usage data?","slug":"why-does-atom-collect-usage-data","link":"#why-does-atom-collect-usage-data","children":[]},{"level":3,"title":"Atom in the cloud?","slug":"atom-in-the-cloud","link":"#atom-in-the-cloud","children":[]},{"level":3,"title":"What's the difference between an IDE and an editor?","slug":"what-s-the-difference-between-an-ide-and-an-editor","link":"#what-s-the-difference-between-an-ide-and-an-editor","children":[]},{"level":3,"title":"How can I tell if subpixel antialiasing is working?","slug":"how-can-i-tell-if-subpixel-antialiasing-is-working","link":"#how-can-i-tell-if-subpixel-antialiasing-is-working","children":[]},{"level":3,"title":"Why is Atom deleting trailing whitespace? Why is there a newline at the end of the file?","slug":"why-is-atom-deleting-trailing-whitespace-why-is-there-a-newline-at-the-end-of-the-file","link":"#why-is-atom-deleting-trailing-whitespace-why-is-there-a-newline-at-the-end-of-the-file","children":[]},{"level":3,"title":"What does Safe Mode do?","slug":"what-does-safe-mode-do","link":"#what-does-safe-mode-do","children":[]},{"level":3,"title":"I have a question about a specific Atom community package. Where is the best place to ask it?","slug":"i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it","link":"#i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it","children":[]},{"level":3,"title":"I\u2019m using an international keyboard and keys that use AltGr or Ctrl+Alt aren\u2019t working","slug":"i-m-using-an-international-keyboard-and-keys-that-use-altgr-or-ctrl-alt-aren-t-working","link":"#i-m-using-an-international-keyboard-and-keys-that-use-altgr-or-ctrl-alt-aren-t-working","children":[]},{"level":3,"title":"I\u2019m having a problem with Julia! What do I do?","slug":"i-m-having-a-problem-with-julia-what-do-i-do","link":"#i-m-having-a-problem-with-julia-what-do-i-do","children":[]},{"level":3,"title":"I\u2019m getting an error about a \u201Cself-signed certificate\u201D. What do I do?","slug":"i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do","link":"#i-m-getting-an-error-about-a-self-signed-certificate-what-do-i-do","children":[]},{"level":3,"title":"I\u2019m having a problem with PlatformIO! What do I do?","slug":"i-m-having-a-problem-with-platformio-what-do-i-do","link":"#i-m-having-a-problem-with-platformio-what-do-i-do","children":[]},{"level":3,"title":"How do I make Atom recognize a file with extension X as language Y?","slug":"how-do-i-make-atom-recognize-a-file-with-extension-x-as-language-y","link":"#how-do-i-make-atom-recognize-a-file-with-extension-x-as-language-y","children":[]},{"level":3,"title":"How do I make the Welcome screen stop showing up?","slug":"how-do-i-make-the-welcome-screen-stop-showing-up","link":"#how-do-i-make-the-welcome-screen-stop-showing-up","children":[]},{"level":3,"title":"How do I preview web page changes automatically?","slug":"how-do-i-preview-web-page-changes-automatically","link":"#how-do-i-preview-web-page-changes-automatically","children":[]},{"level":3,"title":"How do I accept input from my program or script when using the script package?","slug":"how-do-i-accept-input-from-my-program-or-script-when-using-the-script-package","link":"#how-do-i-accept-input-from-my-program-or-script-when-using-the-script-package","children":[]},{"level":3,"title":"I am unable to update to the latest version of Atom on macOS. How do I fix this?","slug":"i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this","link":"#i-am-unable-to-update-to-the-latest-version-of-atom-on-macos-how-do-i-fix-this","children":[]},{"level":3,"title":"I\u2019m trying to change my syntax colors from styles.less, but it isn\u2019t working!","slug":"i-m-trying-to-change-my-syntax-colors-from-styles-less-but-it-isn-t-working","link":"#i-m-trying-to-change-my-syntax-colors-from-styles-less-but-it-isn-t-working","children":[]},{"level":3,"title":"How do I build or execute code I've written in Atom?","slug":"how-do-i-build-or-execute-code-i-ve-written-in-atom","link":"#how-do-i-build-or-execute-code-i-ve-written-in-atom","children":[]},{"level":3,"title":"How do I uninstall Atom on macOS?","slug":"how-do-i-uninstall-atom-on-macos","link":"#how-do-i-uninstall-atom-on-macos","children":[]},{"level":3,"title":"MacOS Mojave font rendering change","slug":"macos-mojave-font-rendering-change","link":"#macos-mojave-font-rendering-change","children":[]},{"level":3,"title":"Why does macOS say that Atom wants to access my calendar, contacts, photos, etc.?","slug":"why-does-macos-say-that-atom-wants-to-access-my-calendar-contacts-photos-etc","link":"#why-does-macos-say-that-atom-wants-to-access-my-calendar-contacts-photos-etc","children":[]},{"level":3,"title":"How do I turn on line wrap?","slug":"how-do-i-turn-on-line-wrap","link":"#how-do-i-turn-on-line-wrap","children":[]},{"level":3,"title":"The menu bar disappeared, how do I get it back?","slug":"the-menu-bar-disappeared-how-do-i-get-it-back","link":"#the-menu-bar-disappeared-how-do-i-get-it-back","children":[]},{"level":3,"title":"How do I use a newline in the result of find and replace?","slug":"how-do-i-use-a-newline-in-the-result-of-find-and-replace","link":"#how-do-i-use-a-newline-in-the-result-of-find-and-replace","children":[]},{"level":3,"title":"What is this line on the right in the editor view?","slug":"what-is-this-line-on-the-right-in-the-editor-view","link":"#what-is-this-line-on-the-right-in-the-editor-view","children":[]}]}],"git":{"updatedTime":1669438016000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":3},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"confused-Techie","email":"dev@lhbasics.com","commits":1}]},"readingTime":{"minutes":1.5,"words":449},"filePathRelative":"docs/atom-archive/faq/index.md"}`);export{e as data}; diff --git a/assets/index.html.f1050665.js b/assets/index.html.f1050665.js new file mode 100644 index 0000000000..a8344cc5fa --- /dev/null +++ b/assets/index.html.f1050665.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-04523903","path":"/docs/packages/core/github/","title":"GitHub","lang":"en-us","frontmatter":{"lang":"en-us","title":"GitHub"},"excerpt":"","headers":[{"level":2,"title":"Roadmaps","slug":"roadmaps","link":"#roadmaps","children":[]},{"level":2,"title":"Monthly Planning","slug":"monthly-planning","link":"#monthly-planning","children":[]}],"git":{"updatedTime":1664411672000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":4},{"name":"Joshua MIller","email":"16845458+kaosine@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.34,"words":101},"filePathRelative":"docs/packages/core/github/index.md"}');export{e as data}; diff --git a/assets/index.html.f2a9140b.js b/assets/index.html.f2a9140b.js new file mode 100644 index 0000000000..f02d9a5c91 --- /dev/null +++ b/assets/index.html.f2a9140b.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-625b0d60","path":"/docs/resources/conduct/","title":"Code of Conduct","lang":"en-US","frontmatter":{"title":"Code of Conduct"},"excerpt":"","headers":[{"level":2,"title":"Our Pledge","slug":"our-pledge","link":"#our-pledge","children":[]},{"level":2,"title":"Our Standards","slug":"our-standards","link":"#our-standards","children":[]},{"level":2,"title":"Our Responsibilities","slug":"our-responsibilities","link":"#our-responsibilities","children":[]},{"level":2,"title":"Scope","slug":"scope","link":"#scope","children":[]},{"level":2,"title":"Enforcement","slug":"enforcement","link":"#enforcement","children":[]},{"level":2,"title":"Attribution","slug":"attribution","link":"#attribution","children":[]}],"git":{"updatedTime":1669429078000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.09,"words":26},"filePathRelative":"docs/resources/conduct/index.md"}');export{e as data}; diff --git a/assets/index.html.f2ffe19c.js b/assets/index.html.f2ffe19c.js new file mode 100644 index 0000000000..2a18e22780 --- /dev/null +++ b/assets/index.html.f2ffe19c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6566cb89","path":"/docs/packages/core/","title":"Core Packages Documentation","lang":"en-us","frontmatter":{"lang":"en-us","title":"Core Packages Documentation","description":"Wiki Resource for many First Party Packages"},"excerpt":"","headers":[{"level":2,"title":"Archived Wikis","slug":"archived-wikis","link":"#archived-wikis","children":[]}],"git":{"updatedTime":1663961800000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.46,"words":139},"filePathRelative":"docs/packages/core/index.md"}');export{e as data}; diff --git a/assets/index.html.f452234d.js b/assets/index.html.f452234d.js new file mode 100644 index 0000000000..4b38958a89 --- /dev/null +++ b/assets/index.html.f452234d.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6df8120e","path":"/docs/packages/core/autocomplete-plus/","title":"Autocomplete-Plus","lang":"en-us","frontmatter":{"lang":"en-us","title":"Autocomplete-Plus"},"excerpt":"","headers":[],"git":{"updatedTime":1664399092000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.22,"words":67},"filePathRelative":"docs/packages/core/autocomplete-plus/index.md"}');export{e as data}; diff --git a/assets/index.html.f7363831.js b/assets/index.html.f7363831.js new file mode 100644 index 0000000000..df99f5c2cd --- /dev/null +++ b/assets/index.html.f7363831.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-923287ec","path":"/docs/resources/tooling/","title":"Tooling","lang":"en-US","frontmatter":{"title":"Tooling"},"excerpt":"","headers":[{"level":2,"title":"Continuous Integration","slug":"continuous-integration","link":"#continuous-integration","children":[{"level":3,"title":"Cirrus CI","slug":"cirrus-ci","link":"#cirrus-ci","children":[]},{"level":3,"title":"Codacy","slug":"codacy","link":"#codacy","children":[]}]},{"level":2,"title":"i18n (Internationalization)","slug":"i18n-internationalization","link":"#i18n-internationalization","children":[{"level":3,"title":"Crowdin","slug":"crowdin","link":"#crowdin","children":[]}]},{"level":2,"title":"Package Managers","slug":"package-managers","link":"#package-managers","children":[]},{"level":2,"title":"Cloud Database","slug":"cloud-database","link":"#cloud-database","children":[]},{"level":2,"title":"Cloud Compute","slug":"cloud-compute","link":"#cloud-compute","children":[]},{"level":2,"title":"Additional Testing tools","slug":"additional-testing-tools","link":"#additional-testing-tools","children":[{"level":3,"title":"Action Pulsar Dependency Tester","slug":"action-pulsar-dependency-tester","link":"#action-pulsar-dependency-tester","children":[]}]}],"git":{"updatedTime":1667932127000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.08,"words":24},"filePathRelative":"docs/resources/tooling/index.md"}');export{e as data}; diff --git a/assets/index.html.f7de1c88.js b/assets/index.html.f7de1c88.js new file mode 100644 index 0000000000..7aa3b14197 --- /dev/null +++ b/assets/index.html.f7de1c88.js @@ -0,0 +1,188 @@ +import{_ as h,a as u,b as m,c as f}from"./unity-theme.516a5ae9.js";import{_ as g,a as b}from"./symbol.b88bf5a9.js";import{_ as k,a as y,b as v}from"./encodings.f93acf64.js";import{_ as w,a as _}from"./find-replace-project.ada882a7.js";import{_ as x,a as C}from"./snippet-scope.1f381b52.js";import{_ as A}from"./autocomplete.b0bae406.js";import{_ as S}from"./folding.6d09f8df.js";import{_ as q}from"./panes.a7cec271.js";import{_ as T}from"./allow-pending-pane-items.0fe3dd4a.js";import{_ as I}from"./grammar.c16b8cd6.js";import{_ as P,a as F,b as R,c as L,d as G,e as H}from"./open-on-github.6fa9b166.js";import{_ as O,a as M,b as j,c as z,d as E,e as D,f as Y,g as $,h as W,i as U,j as B,k as N,l as V,m as J,n as K,o as Z,p as Q,q as X,r as ee,s as te,t as ae,u as oe,v as ne,w as se,x as ie,y as le}from"./github-review-reply.6d405e4b.js";import{_ as re,a as ce}from"./preview.2eaf146a.js";import{_ as de}from"./menubar.df752f94.js";import{_ as pe,a as he,b as ue,c as me,d as fe}from"./portable-mode-folder.90669a9a.js";import{_ as ge,o as be,c as ke,a as e,b as t,d as a,w as s,f as n,r as p}from"./app.0e1565ce.js";const ye={},ve=n('STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, you use this at your own risk. Current Pulsar documentation for this section is found at the documentation home.
Now that we've covered the very basics of Atom, we are ready to see how to really start getting the most out of using it. In this chapter we'll look at how to find and install new packages in order to add new functionality, how to find and install new themes, how to work with and manipulate text in a more advanced way, how to customize the editor in any way you wish, how to work with Git for version control and more.
At the end of this chapter, you should have a customized environment that you're comfortable in and you should be able to change, create and move through your text like a master.
This means that packages can be incredibly powerful and can change everything from the very look and feel of the entire interface to the basic operation of even core functionality.
In order to install a new package, you can use the Install tab in the now familiar Settings View. Open up the Settings View using Cmd+,Ctrl+,, click on the "Install" tab and type your search query into the box under Install Packages.
The packages listed here have been published to https://atom.io/packages which is the official registry for Atom packages. Searching on the Settings View will go to the Atom package registry and pull in anything that matches your search terms.
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Atom, it will show up in the Settings View under the "Packages" tab, along with all the preinstalled packages that come with Atom. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Atom will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Atom from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on atom.io, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
',14),Te=n('You can also install packages or themes from the command line using apm
.
Tip
Check that you have apm
installed by running the following command in your terminal:
$ apm help install
+
You should see a message print out with details about the apm install
command.
If you do not, see the Installing Atom section for instructions on how to install the atom
and apm
commands for your system.
You can also install packages by using the apm install
command:
apm install <package_name>
to install the latest version.apm install <package_name>@<package_version>
to install a specific version.You can also use apm
to find new packages to install. If you run apm search
, you can search the package registry for a search term.
$ apm search coffee
+> Search Results For 'coffee' (29)
+> \u251C\u2500\u2500 build-coffee Atom Build provider for coffee, compiles CoffeeScript (1160 downloads, 2 stars)
+> \u251C\u2500\u2500 scallahan-coffee-syntax A coffee inspired theme from the guys over at S.CALLAHAN (183 downloads, 0 stars)
+> \u251C\u2500\u2500 coffee-paste Copy/Paste As : Js \u27A4 Coffee / Coffee \u27A4 Js (902 downloads, 4 stars)
+> \u251C\u2500\u2500 atom-coffee-repl Coffee REPL for Atom Editor (894 downloads, 2 stars)
+> \u251C\u2500\u2500 coffee-navigator Code navigation panel for Coffee Script (3493 downloads, 22 stars)
+> ...
+> \u251C\u2500\u2500 language-iced-coffeescript Iced coffeescript for atom (202 downloads, 1 star)
+> \u2514\u2500\u2500 slontech-syntax Dark theme for web developers ( HTML, CSS/LESS, PHP, MYSQL, javascript, AJAX, coffee, JSON ) (2018 downloads, 3 stars)
+
You can use apm view
to see more information about a specific package.
$ apm view build-coffee
+> build-coffee
+> \u251C\u2500\u2500 0.6.4
+> \u251C\u2500\u2500 https://github.com/idleberg/atom-build-coffee
+> \u251C\u2500\u2500 Atom Build provider for coffee, compiles CoffeeScript
+> \u251C\u2500\u2500 1152 downloads
+> \u2514\u2500\u2500 2 stars
+>
+> Run \`apm install build-coffee\` to install this package.
+
While it's pretty easy to move around Atom by clicking with the mouse or using the arrow keys, there are some keybindings that may help you keep your hands on the keyboard and navigate around a little faster.
`,6),Le=e("p",null,"Atom has support for all the standard Linux cursor movement key combinations. To go up, down, left or right a single character you can use the arrow keys.",-1),Ge=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),He=e("p",null,[t("Atom ships with many of the basic Emacs keybindings for navigating a document. To go up and down a single character, you can use "),e("kbd",{class:"platform-mac"},"Ctrl+P"),t(" and "),e("kbd",{class:"platform-mac"},"Ctrl+N"),t(". To go left and right a single character, you can use "),e("kbd",{class:"platform-mac"},"Ctrl+B"),t(" and "),e("kbd",{class:"platform-mac"},"Ctrl+F"),t(". These are the equivalent of using the arrow keys, though some people prefer not having to move their hands to where the arrow keys are located on their keyboard.")],-1),Oe=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),Me=e("p",null,"Atom has support for all the standard Windows cursor movement key combinations. To go up, down, left or right a single character you can use the arrow keys.",-1),je=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),ze=n('You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press Cmd+RCtrl+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to Cmd+TCtrl+T. You can also search for symbols across your project but it requires a tags
file.
Atom also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press Cmd+F2Alt+Ctrl+F2Ctrl+Shift+F2, Atom will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Atom will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
',6),gt={href:"https://github.com/atom/bookmarks",target:"_blank",rel:"noopener noreferrer"},bt=n('Text selections in Atom support a number of actions, such as scoping deletion, indentation and search actions, and marking text for actions such as quoting and bracketing.
Selections mirror many of the movement commands. They're actually exactly the same keybindings as the movement commands, but with a Shift key added in.
In addition to the cursor movement selection commands, there are also a few commands that help with selecting specific areas of content.
So far we've looked at a number of ways to move around and select regions of a file, so now let's actually change some of that text. Obviously you can type in order to insert characters, but there are also a number of ways to delete and manipulate text that could come in handy.
There are a handful of cool keybindings for basic text manipulation that might come in handy. These range from moving around lines of text and duplicating lines to changing the case.
Atom also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using Alt+Cmd+QAlt+Ctrl+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
One of the cool things that Atom can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.
Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any plugin or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the CmdCtrl key pressed down to select multiple regions of your text simultaneously.
Atom comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
',6),St={href:"https://github.com/atom/whitespace",target:"_blank",rel:"noopener noreferrer"},qt=e("code",null,"whitespace",-1),Tt=n('Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Atom, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Atom will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Atom ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Atom will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, \u201C\u201D
, \u2018\u2019
, \xAB\xBB
, \u2039\u203A
, and backticks when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Atom will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
Atom also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.
If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.
When you open a file, Atom will try to auto-detect the encoding. If Atom can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.
If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.
',7),Ft={href:"https://github.com/atom/encoding-selector",target:"_blank",rel:"noopener noreferrer"},Rt=n('Finding and replacing text in your file or project is quick and easy in Atom.
If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press Cmd+FCtrl+F, type in a search string and press Enter (or Cmd+GF3 or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Scott" with the string "Dragon", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
',7),Lt={class:"custom-container note"},Gt=e("p",{class:"custom-container-title"},"Note",-1),Ht=e("p",null,[e("strong",null,"Note:"),t(" Atom uses JavaScript regular expressions to perform regular expression searches.")],-1),Ot={href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions",target:"_blank",rel:"noopener noreferrer"},Mt=e("p",null,[t("You can also find and replace throughout your entire project if you invoke the panel with "),e("kbd",{class:"platform-mac"},"Cmd+Shift+F"),e("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+F"),t(".")],-1),jt=e("p",null,[e("img",{src:w,alt:"Find and replace text in your project",title:"Find and replace text in your project"})],-1),zt=e("p",null,"This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.",-1),Et={href:"https://en.wikipedia.org/wiki/Glob_%28programming%29",target:"_blank",rel:"noopener noreferrer"},Dt=e("code",null,"src/*.js",-1),Yt=e("code",null,"src",-1),$t=e("code",null,"**",-1),Wt=e("code",null,"docs/**/*.md",-1),Ut=e("code",null,"docs/a/foo.md",-1),Bt=e("code",null,"docs/a/b/foo.md",-1),Nt=e("p",null,[t("When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders "),e("code",null,"/path1/folder1"),t(" and "),e("code",null,"/path2/folder2"),t(" open, you could enter a pattern starting with "),e("code",null,"folder1"),t(" to search only in the first folder.")],-1),Vt=e("p",null,[t("Press "),e("kbd",{class:"platform-all"},"Esc"),t(" while focused on the Find and Replace panel to clear the pane from your workspace.")],-1),Jt={href:"https://github.com/atom/find-and-replace",target:"_blank",rel:"noopener noreferrer"},Kt={href:"https://github.com/atom/scandal",target:"_blank",rel:"noopener noreferrer"},Zt=n(`Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Atom, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your ~/.atom
%USERPROFILE%\\.atom
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Atom. You can also easily open up that file by selecting the Atom > SnippetsEdit > SnippetsFile > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (\${1:true}) {
+ $2
+ } else if (\${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
\u{1F4A5} just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Atom should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
If you're still looking to save some typing time, Atom also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
',6),ca={href:"https://github.com/atom/autocomplete-plus",target:"_blank",rel:"noopener noreferrer"},da=n('If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.
You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the Alt+Cmd+[Alt+Ctrl+[ and Alt+Cmd+]Alt+Ctrl+] keybindings.
To fold everything, use Alt+Cmd+Shift+[Alt+Ctrl+Shift+[ and to unfold everything use Alt+Cmd+Shift+]Alt+Ctrl+Shift+]. You can also fold at a specific indentation level with Cmd+KCtrl+K Cmd+0-9Ctrl+0-9 where the number is the indentation depth.
Finally, you can fold arbitrary sections of your code or text by making a selection and then typing Alt+Cmd+Ctrl+FAlt+Ctrl+F or choosing "Fold Selection" in the Command Palette.
You can split any editor pane horizontally or vertically by using Cmd+KCtrl+K Up/Down/Left/Right where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with Cmd+KCtrl+K Cmd+Up/Down/Left/RightCtrl+Up/Down/Left/Right where the direction is the direction the focus should move to.
Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.
',10),pa={class:"custom-container tip"},ha=e("p",{class:"custom-container-title"},"Tip",-1),ua={href:"https://github.com/atom/tabs",target:"_blank",rel:"noopener noreferrer"},ma=n('To close a pane, you can close all pane items with Cmd+WCtrl+W. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.
"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
In order to use version control in Atom, the project root needs to contain the Git repository.
The Alt+Cmd+ZAlt+Ctrl+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use Cmd+ZCtrl+Z afterwards to restore the previous contents.
You can configure Atom to be your Git commit editor with the following command:
$ git config --global core.editor "atom --wait"
+
This package also adds Alt+G Down and Alt+G Up keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
The github package brings Git and GitHub integration right inside Atom.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Atom and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the Cmd-LeftCtrl-Left or Cmd-RightCtrl-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "\u{1F464}\u2795" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Atom will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting Cmd+Shift+;Ctrl+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Atom will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Atom ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press Cmd+CCtrl+CCtrl+Ins or if you right-click in the preview pane and choose "Copy as HTML".
',6),Za={href:"https://github.com/atom/markdown-preview",target:"_blank",rel:"noopener noreferrer"},Qa=n(`There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing "Snippets: Available" in the Command Palette.
Now that we are feeling comfortable with just about everything built into Atom, let's look at how to tweak it. Perhaps there is a keybinding that you use a lot but feels wrong or a color that isn't quite right for you. Atom is amazingly flexible, so let's go over some of the simpler flexes it can do.
key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Atom's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(\${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your ~/.atom
%USERPROFILE%\\.atom
directory. You can open this file in an editor from the Atom > StylesheetFile > StylesheetEdit > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing Alt+Cmd+ICtrl+Shift+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Atom. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
',5),io={class:"custom-container tip"},lo=e("p",{class:"custom-container-title"},"Tip",-1),ro={href:"http://www.lesscss.org",target:"_blank",rel:"noopener noreferrer"},co=e("p",null,[t("If you prefer to use CSS instead, you can do that in the same "),e("code",null,"styles.less"),t(" file, since CSS is also valid in Less.")],-1),po=n(`Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Atom keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Atom's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Atom is started. It will always be loaded last, giving you the chance to override bindings that are defined by Atom's core keymaps or third-party packages. You can open this file in an editor from the Atom > KeymapFile > KeymapEdit > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Atom in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the Cmd+.Ctrl+. key combination. It will show you what keys Atom saw you press and what command Atom executed because of that combination.
Atom loads configuration settings from the config.cson
file in your ~/.atom
%USERPROFILE%\\.atom
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the Atom > ConfigFile > ConfigEdit > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of AtomprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \\n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Atom to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press Cmd+Shift+PCtrl+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and press Alt+Cmd+Pchoose "Editor: Log Cursor Scope" in the Command Palette to show all scopes for the current position of the cursor. The scope mentioned top most is always the language for this kind of file, the scopes following are specific to the cursor position:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Atom to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Atom are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Atom Home directory will, by default, be called .atom
and will be located in the root of the home directory of the user.
An environment variable can be used to make Atom use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Atom Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Atom settings.
In addition to using the ATOM_HOME
environment variable, Atom can also be set to use "Portable Mode".
Portable Mode is most useful for taking Atom with you, with all your custom setting and packages, from machine to machine. This may take the form of keeping Atom on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Atom is in Portable Mode when there is a directory named .atom sibling to the directory in which the atom executable file lives. For example, the installed Atom directory can be placed into a Dropbox folder next to a .atom folder.
With such a setup, Atom will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Atom provides a command-line parameter option for setting Portable Mode.
$ atom --portable
+
Executing atom with the --portable
option will take the .atom
directory you have in the default location (~/.atom
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
At this point you should be something of an Atom master user. You should be able to navigate and manipulate your text and files like a wizard. You should also be able to customize Atom backwards and forwards to make it look and act just how you want it to.
In the next chapter, we're going to kick it up a notch: we'll take a look at changing and adding new functionality to the core of Atom itself. We're going to start creating packages for Atom. If you can dream it, you can build it.
`,52);function ho(uo,mo){const o=p("ExternalLinkIcon"),d=p("RouterLink"),c=p("Tabs");return be(),ke("div",null,[ve,e("p",null,[t("First we'll start with the Atom package system. As we mentioned previously, Atom itself is a very basic core of functionality that ships with a number of useful packages that add new features like the "),e("a",we,[t("Tree View"),a(o)]),t(" and the "),e("a",_e,[t("Settings View"),a(o)]),t(".")]),e("p",null,[t("In fact, there are more than 80 packages that comprise all of the functionality that is available in Atom by default. For example, the "),e("a",xe,[t("Welcome screen"),a(o)]),t(" that you see when you first start Atom, the "),e("a",Ce,[t("spell checker"),a(o)]),t(", the "),e("a",Ae,[t("themes"),a(o)]),t(" and the "),e("a",Se,[t("Fuzzy Finder"),a(o)]),t(" are all packages that are separately maintained and all use the same APIs that you have access to, as we'll see in great detail in "),a(d,{to:"/hacking-atom/"},{default:s(()=>[t("Hacking Atom")]),_:1}),t(".")]),qe,e("p",null,[t('Clicking on "Install" will install the theme and make it available in the Theme dropdowns as we saw in '),a(d,{to:"/getting-started/sections/atom-basics/#changing-the-theme"},{default:s(()=>[t("Changing the Theme")]),_:1}),t(".")]),Te,e("p",null,[t("For example "),Ie,t(" installs the "),Pe,t(" release of the "),e("a",Fe,[t("Emmet"),a(o)]),t(" package.")]),Re,a(c,{id:"124",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:s(({title:i,value:l,isActive:r})=>[Le,Ge]),tab1:s(({title:i,value:l,isActive:r})=>[He,Oe]),tab2:s(({title:i,value:l,isActive:r})=>[Me,je]),_:1}),ze,e("p",null,[t("Atom also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the "),a(d,{to:"/getting-started/sections/atom-basics/#command-palette"},{default:s(()=>[t("Command Palette")]),_:1}),t(", but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your "),Ee,t(" to create a key combination. You can open "),De,t(" file in an editor from the "),Ye,$e,We,t(" menu.")]),Ue,a(c,{id:"197",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:s(({title:i,value:l,isActive:r})=>[Be]),tab1:s(({title:i,value:l,isActive:r})=>[Ne]),tab2:s(({title:i,value:l,isActive:r})=>[Ve]),_:1}),e("p",null,[t("This will bind the command "),Je,t(" to "),Ke,Ze,t(". For more information on customizing your keybindings, see "),a(d,{to:"/using-atom/sections/basic-customization/#customizing-keybindings"},{default:s(()=>[t("Customizing Keybindings")]),_:1}),t(".")]),Qe,a(c,{id:"214",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:s(({title:i,value:l,isActive:r})=>[Xe]),tab1:s(({title:i,value:l,isActive:r})=>[et]),tab2:s(({title:i,value:l,isActive:r})=>[tt]),_:1}),at,e("p",null,[t("You can generate a "),ot,t(" file by using the "),e("a",nt,[t("ctags utility"),a(o)]),t(". Once it is installed, you can use it to generate a "),st,t(" file by running a command to generate it. See the "),e("a",it,[t("ctags documentation"),a(o)]),t(" for details.")]),a(c,{id:"237",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:s(({title:i,value:l,isActive:r})=>[lt]),tab1:s(({title:i,value:l,isActive:r})=>[rt]),tab2:s(({title:i,value:l,isActive:r})=>[ct]),_:1}),e("p",null,[t("You can customize how tags are generated by creating your own "),dt,t(" file in your home directory, "),pt,ht,t(". An example can be found "),e("a",ut,[t("here"),a(o)]),t(".")]),e("p",null,[t("The symbols navigation functionality is implemented in the "),e("a",mt,[t("symbols-view"),a(o)]),t(" package.")]),ft,t(' ![View and filter bookmarks](@images/atom/bookmarks.png "View and filter bookmarks") '),e("p",null,[t("The bookmarks functionality is implemented in the "),e("a",gt,[t("bookmarks"),a(o)]),t(" package.")]),bt,a(c,{id:"357",data:[{title:"Mac"}],"tab-id":"atom-selections"},{tab0:s(({title:i,value:l,isActive:r})=>[kt]),_:1}),yt,a(c,{id:"409",data:[{title:"Mac"}],"tab-id":"editing-and-deleting"},{tab0:s(({title:i,value:l,isActive:r})=>[vt]),_:1}),wt,a(c,{id:"446",data:[{title:"Mac"}],"tab-id":"editing-and-deleting"},{tab0:s(({title:i,value:l,isActive:r})=>[_t]),_:1}),xt,a(c,{id:"495",data:[{title:"Mac"}],"tab-id":"editing-and-deleting"},{tab0:s(({title:i,value:l,isActive:r})=>[Ct]),_:1}),At,e("p",null,[t("The whitespace commands are implemented in the "),e("a",St,[t("atom/whitespace"),a(o)]),t(" package. The settings for the whitespace commands are managed on the page for the "),qt,t(" package.")]),Tt,e("p",null,[t("The brackets functionality is implemented in the "),e("a",It,[t("bracket-matcher"),a(o)]),t(" package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.")]),Pt,e("p",null,[t("The encoding selector is implemented in the "),e("a",Ft,[t("encoding-selector"),a(o)]),t(" package.")]),Rt,e("div",Lt,[Gt,Ht,e("p",null,[t("When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, \u2026 $&. Refer to JavaScript's "),e("a",Ot,[t("guide to regular expressions"),a(o)]),t(" to learn more about regular expression syntax you can use in Atom.")])]),Mt,jt,zt,e("p",null,[t("You can limit a search to a subset of the files in your project by entering a "),e("a",Et,[t("glob pattern"),a(o)]),t(' into the "File/Directory pattern" text box. For example, the pattern '),Dt,t(" would restrict the search to JavaScript files in the "),Yt,t(' directory. The "globstar" pattern ('),$t,t(") can be used to match arbitrarily many subdirectories. For example, "),Wt,t(" will match "),Ut,t(", "),Bt,t(", etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.")]),Nt,Vt,e("p",null,[t("The Find and Replace functionality is implemented in the "),e("a",Jt,[t("find-and-replace"),a(o)]),t(" package and uses the "),e("a",Kt,[t("scandal"),a(o)]),t(" Node module to do the actual searching.")]),Zt,e("div",Qt,[Xt,e("p",null,[t("Snippet keys, unlike CSS selectors, can only be repeated once per level. If there are duplicate keys at the same level, then only the last one will be read. See "),a(d,{to:"/using-atom/sections/basic-customization/#configuring-with-cson"},{default:s(()=>[t("Configuring with CSON")]),_:1}),t(" for more information.")])]),ea,e("p",null,[t("You can also use "),e("a",ta,[t("CoffeeScript multi-line syntax"),a(o)]),t(" using "),aa,t(" for larger templates:")]),oa,e("p",null,[t("Again, see "),a(d,{to:"/using-atom/sections/basic-customization/#configuring-with-cson"},{default:s(()=>[t("Configuring with CSON")]),_:1}),t(" for more information on CSON key structure and non-repeatability.")]),na,e("p",null,[t("The snippets functionality is implemented in the "),e("a",sa,[t("snippets"),a(o)]),t(" package.")]),e("p",null,[t("For more examples, see the snippets in the "),e("a",ia,[t("language-html"),a(o)]),t(" and "),e("a",la,[t("language-javascript"),a(o)]),t(" packages.")]),ra,e("p",null,[t("The Autocomplete functionality is implemented in the "),e("a",ca,[t("autocomplete-plus"),a(o)]),t(" package.")]),da,e("div",pa,[ha,e("p",null,[t("If you don't like using tabs, you don't have to. You can disable the "),e("a",ua,[t("tabs package"),a(o)]),t(" and each pane will still support multiple pane items. You just won't have tabs to use to click between them.")])]),ma,e("p",null,[t('The "grammar" of a file is what language Atom has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in '),a(d,{to:"/using-atom/sections/snippets/"},{default:s(()=>[t("Snippets")]),_:1}),t(".")]),fa,ga,ba,ka,e("p",null,[t("The Grammar Selector functionality is implemented in the "),e("a",ya,[t("grammar-selector"),a(o)]),t(" package.")]),va,e("p",null,[t("Version control is an important aspect of any project and Atom comes with basic "),e("a",wa,[t("Git"),a(o)]),t(" and "),e("a",_a,[t("GitHub"),a(o)]),t(" integration built in.")]),xa,e("p",null,[t("Atom ships with the "),e("a",Ca,[t("fuzzy-finder package"),a(o)]),t(" which provides "),Aa,Sa,t(" to quickly open files in the project and "),qa,Ta,t(" to jump to any open editor. The package also provides "),Ia,Pa,t(" which displays a list of all the untracked and modified files in the project. These will be the same files that you would see on the command line if you ran "),Fa,t(".")]),Ra,La,Ga,e("p",null,[t("Atom can be used as your Git commit editor and ships with the "),e("a",Ha,[t("language-git package"),a(o)]),t(" which adds syntax highlighting to edited commit, merge, and rebase messages.")]),Oa,e("p",null,[t("The "),e("a",Ma,[t("language-git"),a(o)]),t(" package will help remind you to be brief by colorizing the first lines of commit messages when they're longer than 50 or 65 characters.")]),ja,e("p",null,[t("The "),e("a",za,[t("status-bar"),a(o)]),t(" package that ships with Atom includes several Git decorations that display on the right side of the status bar:")]),Ea,Da,Ya,e("p",null,[t("The included "),e("a",$a,[t("git-diff"),a(o)]),t(" package colorizes the gutter next to lines that have been added, edited, or removed.")]),Wa,e("p",null,[t("Learn more about "),e("a",Ua,[t("merge vs. rebase"),a(o)]),t(".")]),Ba,e("p",null,[t("Though it is probably most common to use Atom to write software code, Atom can also be used to write prose quite effectively. Most often this is done in some sort of markup language such as Asciidoc or "),e("a",Na,[t("Markdown"),a(o)]),t(" (in which this manual is written). Here we'll quickly cover a few of the tools Atom provides for helping you write prose.")]),Va,e("p",null,[t("The spell checking is implemented in the "),e("a",Ja,[t("spell-check"),a(o)]),t(" package.")]),Ka,e("p",null,[t("Markdown preview is implemented in the "),e("a",Za,[t("markdown-preview"),a(o)]),t(" package.")]),Qa,e("p",null,[t("All of Atom's config files (with the exception of your "),Xa,t(" and your "),eo,t(") are written in CSON, short for "),e("a",to,[t("CoffeeScript Object Notation"),a(o)]),t(". Just like its namesake JSON, "),e("a",ao,[t("JavaScript Object Notation"),a(o)]),t(", CSON is a text format for storing structured data in the form of simple objects made up of key-value pairs.")]),oo,a(c,{id:"1512",data:[{title:"Mac"}],"tab-id":"basic-customization"},{tab0:s(({title:i,value:l,isActive:r})=>[no]),_:1}),so,e("div",io,[lo,e("p",null,[t("If you are unfamiliar with Less, it is a basic CSS preprocessor that makes some things in CSS a bit easier. You can learn more about it at "),e("a",ro,[t("lesscss.org"),a(o)]),t(".")]),co]),po])}const Fo=ge(ye,[["render",ho],["__file","index.html.vue"]]);export{Fo as default}; diff --git a/assets/index.html.f8b7ceda.js b/assets/index.html.f8b7ceda.js new file mode 100644 index 0000000000..fe016d4581 --- /dev/null +++ b/assets/index.html.f8b7ceda.js @@ -0,0 +1 @@ +import{_ as e,o as c,c as t}from"./app.0e1565ce.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html.fa010113.js b/assets/index.html.fa010113.js new file mode 100644 index 0000000000..736426b5bd --- /dev/null +++ b/assets/index.html.fa010113.js @@ -0,0 +1 @@ +import{_ as t,o,c,a as e,b as s}from"./app.0e1565ce.js";const n={},a=e("div",{class:"custom-container warning"},[e("p",{class:"custom-container-title"},"Under Construction"),e("p",null,[s("This document is under construction, please check back soon for updates. Please see "),e("a",{href:"/docs/community"},"our socials"),s(" and feel free to ask for assistance or inquire as to the status of this document.")])],-1),r=[a];function i(d,l){return o(),c("div",null,r)}const _=t(n,[["render",i],["__file","index.html.vue"]]);export{_ as default}; diff --git a/assets/index.html.fb13f8c6.js b/assets/index.html.fb13f8c6.js new file mode 100644 index 0000000000..218d5c4b47 --- /dev/null +++ b/assets/index.html.fb13f8c6.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6486a861","path":"/tag/regular/","title":"regular Tag","lang":"en-US","frontmatter":{"title":"regular Tag","blog":{"type":"category","name":"regular","key":"tag"},"layout":"Blog"},"excerpt":"","headers":[],"git":{},"readingTime":{"minutes":0,"words":0},"filePathRelative":null}');export{e as data}; diff --git a/assets/index.html.fb3f4ca6.js b/assets/index.html.fb3f4ca6.js new file mode 100644 index 0000000000..be8c9472cd --- /dev/null +++ b/assets/index.html.fb3f4ca6.js @@ -0,0 +1,292 @@ +import{_ as d}from"./keybinding.964a4e0d.js";import{_ as u}from"./markup.30f112ce.js";import{_ as m,o as k,c as h,a as n,b as s,d as e,w as o,f as t,r as l}from"./app.0e1565ce.js";const v={},g=t(`STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
Now that we've written a number of packages and themes, let's take minute to take a closer look at some of the ways that Atom works in greater depth. Here we'll go into more of a deep dive on individual internal APIs and systems of Atom, even looking at some Atom source to see how things are really getting done.
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with \`config.get\`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
The atom.config
database is populated on startup from ~/.atom/config.cson
%USERPROFILE%\\.atom\\config.cson
, but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and Alt+BackspaceCtrl+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (Cmd+Shift+PCtrl+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Atom, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.coffee
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for Cmd+OCtrl+O when the selection is inside an editor pane:
::: codetabs#keymaps-in-depth
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
'atom-text-editor':
+ 'cmd-o': 'abort!'
+
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
:::
But if you click inside the Tree View and press Cmd+OCtrl+O, it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.You can add the following to your init.coffee
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
Or if you've converted your init script to JavaScript:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Atom to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
Atom supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
`,2),X={href:"https://atom.io/docs/api/latest/TextEditor#instance-getRootScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},Z=n("code",null,"Editor::getRootScopeDescriptor",-1),nn=n("code",null,'[".source.js"]',-1),sn={href:"https://atom.io/docs/api/latest/TextEditor#instance-scopeDescriptorForBufferPosition",target:"_blank",rel:"noopener noreferrer"},en=n("code",null,"Editor::scopeDescriptorForBufferPosition",-1),an={href:"https://atom.io/docs/api/latest/Cursor#instance-getScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},tn=n("code",null,"Cursor::getScopeDescriptor",-1),on=n("code",null,'["source.js", "meta.function.js", "entity.name.function.js"]',-1),pn=t(`Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer\u2014the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Atom to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Atom will only attempt to call deserialize if the two versions match, and otherwise return undefined. We plan on implementing a migration system in the future, but this at least protects you from improperly deserializing old state.
Atom contains a number of packages that are Node modules instead of Atom packages. If you want to make changes to the Node modules, for instance atom-keymap
, you have to link them into the development environment differently than you would a normal Atom package.
Here are the steps to run a local version of a Node module within Atom. We're using atom-keymap
as an example:
::: tab#developing-node-modules
`,31),cn=t(`$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than ~/github/atom
+# you need to set the following environment variable
+$ export ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than %USERPROFILE%\\github\\atom
+# you need to set the following environment variable
+$ setx ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
:::
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Atom and do the following:
$ cd <em>WHERE YOU CLONED THE NODE MODULE</em>
+$ npm install
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ apm rebuild
+$ atom --dev .
+
Atom packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
Warning
Danger: \u{1F6A8} Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the apm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if apm has taken care of things for you.
`,8),mn={class:"custom-container note"},kn=n("p",{class:"custom-container-title"},"Note",-1),hn=n("strong",null,"Note:",-1),vn={href:"https://github.com",target:"_blank",rel:"noopener noreferrer"},gn=t('When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\\d+\\.\\d+\\.\\d+
^v\\d+\\.\\d+\\.\\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
apm publish --tag tagname
where tagname
must match the name of the tag created in the above stepIf you no longer want to support your package and cannot find anyone to take it over, you can unpublish your package from https://atom.io. For example, if your package is named package-name
then the command you would execute is:
$ apm unpublish <em>package-name</em>
+
This will remove your package from the https://atom.io package registry. Anyone who has already downloaded a copy of your package will still have it and be able to use it, but it will no longer be available for installation by others.
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ apm unpublish <em>package-name@1.2.3</em>
+
This will remove just this particular version from the https://atom.io package registry.
If you need to rename your package for any reason, you can do so with one simple command \u2013 apm publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ apm publish --rename <em>new-package-name</em>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
.atom
directory must be writeable.atom
directory to your portable device.atom
directory - just create a subdirectory called electronUserData
inside .atom
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ apm config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure apm
to use it by running:
$ apm config set https-proxy <em>YOUR_PROXY_ADDRESS</em>
+
You can run apm config get https-proxy
to verify it has been set correctly.
.pulsar
directory must be writeable.pulsar
directory to your portable device.pulsar
directory - just create a subdirectory called electronUserData
inside .pulsar
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ pulsar -p config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure ppm
to use it by running:
$ pulsar -p config set https-proxy <YOUR_PROXY_ADDRESS>
+
You can run pulsar -p config get https-proxy
to verify it has been set correctly.
Pulsar packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
Atom packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
Note
Please note that its possible this is outdated, as its original version was published by @'Katrina Uychaco' on Jun 14, 2017.
core:undo
support Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and LNX/WIN: Ctrl+Backspace - MAC: Alt+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Pulsar, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.js
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for LNX/WIN: Ctrl+O - MAC: Cmd+O when the selection is inside an editor pane:
But if you click inside the Tree View and press LNX/WIN: Ctrl+O - MAC: Cmd+O , it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.You can add the following to your init.js
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
Or if you are still using the init.coffee
file:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Pulsar to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and Alt+BackspaceCtrl+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (Cmd+Shift+PCtrl+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Atom, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.coffee
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for Cmd+OCtrl+O when the selection is inside an editor pane:
::: codetabs#keymaps-in-depth
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
'atom-text-editor':
+ 'cmd-o': 'abort!'
+
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
:::
But if you click inside the Tree View and press Cmd+OCtrl+O, it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.You can add the following to your init.coffee
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
Or if you've converted your init script to JavaScript:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Atom to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
atom-languageclient
Note
Please note that its possible this list is outdated, as its original version was published by @'Damien Guard' on Apr 21, 2018.
Here are the known Pulsar packages that are using atom-languageclient
today:
If this change is something that you dislike, there are a couple workarounds that the community has identified.
defaults write -g CGFontRenderingFontSmoothingDisabled -bool NO
This appears to re-enable the old "LCD font smoothing" option that was removed in Mojave. It is important to note that this is an OS-wide change.
atom-text-editor {
+ font-weight: bold;
+}
+
This has the benefit of being a change local to Atom only if the rest of the OS looks fine to you.
`,2);function f(_,b){const n=l("ExternalLinkIcon");return i(),r("div",null,[h,t("p",null,[e("In "),t("a",d,[e("macOS Mojave v10.14.x"),a(n)]),e(', Apple disabled subpixel antialiasing on all monitors by default. Previous to Mojave, subpixel antialiasing was disabled only on Retina displays or on all displays if the "LCD font smoothing" option was disabled in System Preferences. With this change in Mojave, some users have reported that '),t("a",p,[e('their fonts in Atom appear "thinner" or "dimmer" than they did previously.'),a(n)]),e(" It can look better or worse depending on your font and theme selections, but in all cases this is completely a side-effect of the change that Apple made to their font rendering and is outside Atom's and Electron's control.")]),m,t("p",null,[e("Add the following to "),t("a",u,[e("your stylesheet"),a(n)]),e(":")]),g])}const y=s(c,[["render",f],["__file","macos-mojave-font-rendering-change.html.vue"]]);export{y as default}; diff --git a/assets/macos-mojave-font-rendering-change.html.d33ba74d.js b/assets/macos-mojave-font-rendering-change.html.d33ba74d.js new file mode 100644 index 0000000000..93d1ccc71b --- /dev/null +++ b/assets/macos-mojave-font-rendering-change.html.d33ba74d.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3e42c98a","path":"/docs/atom-archive/faq/sections/macos-mojave-font-rendering-change.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"MacOS Mojave font rendering change","slug":"macos-mojave-font-rendering-change","link":"#macos-mojave-font-rendering-change","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.79,"words":237},"filePathRelative":"docs/atom-archive/faq/sections/macos-mojave-font-rendering-change.md"}');export{e as data}; diff --git a/assets/maintaining-a-fork-of-a-core-package-in-atom-atom.html.81c74f67.js b/assets/maintaining-a-fork-of-a-core-package-in-atom-atom.html.81c74f67.js new file mode 100644 index 0000000000..b514262ce4 --- /dev/null +++ b/assets/maintaining-a-fork-of-a-core-package-in-atom-atom.html.81c74f67.js @@ -0,0 +1,10 @@ +import{_ as i,o as r,c as s,a as e,b as t,d as a,f as n,r as c}from"./app.0e1565ce.js";const d={},m=e("h3",{id:"maintaining-a-fork-of-a-core-package-in-atom-atom",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#maintaining-a-fork-of-a-core-package-in-atom-atom","aria-hidden":"true"},"#"),t(" Maintaining a Fork of a Core Package in atom/atom")],-1),l={href:"https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate-core-packages.md",target:"_blank",rel:"noopener noreferrer"},h={href:"https://github.com/atom/atom",target:"_blank",rel:"noopener noreferrer"},u={href:"https://github.com/atom/one-light-ui",target:"_blank",rel:"noopener noreferrer"},g={href:"https://github.com/atom/atom/tree/master/packages/one-light-ui",target:"_blank",rel:"noopener noreferrer"},p=e("code",null,"packages/one-light-ui",-1),f=e("p",null,"If you forked one of the core packages before it was moved into the atom/atom repository, and you want to continue merging upstream changes into your fork, please follow the steps below.",-1),_=e("h4",{id:"step-by-step-guide",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#step-by-step-guide","aria-hidden":"true"},"#"),t(" Step-by-step guide")],-1),k={href:"https://github.com/atom/one-light-ui",target:"_blank",rel:"noopener noreferrer"},b=e("code",null,"one-light-ui-plus",-1),v=n(`Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
$ git remote add upstream https://github.com/atom/atom.git
+
Tip
Tip: Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the atom/atom repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log upstream/master -- packages/one-light-ui
+8ac9919a0 Bump up border size (Hugh Baht, 17 minutes ago)
+3bf4d226e Remove obsolete build status link in one-light-ui README (Jason Rudolph, 3 days ago)
+3edf64ad0 Merge pull request #42 from atom/sm-select-list (simurai, 2 weeks ago)
+...
+
Look through the log and identify the commits that you want to merge into your fork.
Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
$ git remote add upstream https://github.com/pulsar-edit/pulsar.git
+
Tip
Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the pulsar-edit/pulsar repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log --oneline upstream/master -- packages/one-light-ui
+f884f6de8 [themes] Rename A[a]tom -> P[p]ulsar
+0db3190f4 Additional rebranding where needed
+234adb874 Remove deprecated code strings
+...
+
Look through the log and identify the commits that you want to merge into your fork.
For each commit that you want to bring into your fork, use [git format-patch
][https://git-scm.com/docs/git-format-patch] in conjunction with [git am
][https://git-scm.com/docs/git-am]. For example, to merge commit f884f6de8
into your fork:
$ git format-patch -1 --stdout f884f6de8 | git am -p3
+
Repeat this step for each commit that you want to merge into your fork.
`,12);function A(F,C){const t=c("ExternalLinkIcon");return r(),i("div",null,[l,a("p",null,[e("Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team "),a("a",p,[e("consolidated many core Atom packages"),o(t)]),e(" into the "),a("a",h,[e("atom/atom repository"),o(t)]),e(". For example, the one-light-ui package was originally maintained in the "),a("a",u,[e("atom/one-light-ui"),o(t)]),e(" repository, but was moved to the "),a("a",m,[g,e(" directory"),o(t)]),e(" in the main repository.")]),a("p",null,[e("The Pulsar team has continued this trend and has move even more packages into the core, particularly default language packages. A list of these packages moved can be found in "),a("a",f,[e("this document"),o(t)]),e(".")]),k,b,a("p",null,[e("For the sake of this guide, let's assume that you forked the "),a("a",v,[e("pulsar-edit/one-light-ui"),o(t)]),e(" repository, renamed your fork to "),_,e(", and made some customizations.")]),y,a("p",null,[e("Add the "),a("a",x,[e("pulsar-edit/pulsar repository"),o(t)]),e(" as a git remote:")]),w])}const I=s(d,[["render",A],["__file","maintaining-a-fork-of-a-core-package.html.vue"]]);export{I as default}; diff --git a/assets/maintaining-your-packages.html.4eb25dae.js b/assets/maintaining-your-packages.html.4eb25dae.js new file mode 100644 index 0000000000..9daa8eb0fd --- /dev/null +++ b/assets/maintaining-your-packages.html.4eb25dae.js @@ -0,0 +1 @@ +const a=JSON.parse('{"key":"v-322ec6da","path":"/docs/launch-manual/sections/behind-pulsar/sections/maintaining-your-packages.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Maintaining Your Packages","slug":"maintaining-your-packages","link":"#maintaining-your-packages","children":[{"level":3,"title":"Publishing a Package Manually","slug":"publishing-a-package-manually","link":"#publishing-a-package-manually","children":[]},{"level":3,"title":"Adding a Collaborator","slug":"adding-a-collaborator","link":"#adding-a-collaborator","children":[]},{"level":3,"title":"Transferring Ownership","slug":"transferring-ownership","link":"#transferring-ownership","children":[]},{"level":3,"title":"Unpublish Your Package","slug":"unpublish-your-package","link":"#unpublish-your-package","children":[]},{"level":3,"title":"Rename Your Package","slug":"rename-your-package","link":"#rename-your-package","children":[]}]}],"git":{"updatedTime":1668992424000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4}]},"readingTime":{"minutes":2.79,"words":837},"filePathRelative":"docs/launch-manual/sections/behind-pulsar/sections/maintaining-your-packages.md"}');export{a as data}; diff --git a/assets/maintaining-your-packages.html.641bcc0a.js b/assets/maintaining-your-packages.html.641bcc0a.js new file mode 100644 index 0000000000..016cab26d7 --- /dev/null +++ b/assets/maintaining-your-packages.html.641bcc0a.js @@ -0,0 +1,4 @@ +import{_ as r,o as s,c as i,a as e,b as a,d as n,f as t,r as c}from"./app.0e1565ce.js";const l={},h=t('While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
Warning
Danger: \u{1F6A8} Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the apm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if apm has taken care of things for you.
',5),d={class:"custom-container note"},p=e("p",{class:"custom-container-title"},"Note",-1),u=e("strong",null,"Note:",-1),g={href:"https://github.com",target:"_blank",rel:"noopener noreferrer"},m=t('When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\\d+\\.\\d+\\.\\d+
^v\\d+\\.\\d+\\.\\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
apm publish --tag tagname
where tagname
must match the name of the tag created in the above stepIf you no longer want to support your package and cannot find anyone to take it over, you can unpublish your package from https://atom.io. For example, if your package is named package-name
then the command you would execute is:
$ apm unpublish <em>package-name</em>
+
This will remove your package from the https://atom.io package registry. Anyone who has already downloaded a copy of your package will still have it and be able to use it, but it will no longer be available for installation by others.
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ apm unpublish <em>package-name@1.2.3</em>
+
This will remove just this particular version from the https://atom.io package registry.
If you need to rename your package for any reason, you can do so with one simple command \u2013 apm publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ apm publish --rename <em>new-package-name</em>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
Pre-release information
This section is about a feature in pre-release. The information below documents the intended functionality but there is still ongoing work to support these features with stability.
While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
STOP
Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the ppm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if ppm has taken care of things for you.
',6),h={class:"custom-container note"},d=a("p",{class:"custom-container-title"},"Note",-1),u=a("strong",null,"Note:",-1),g={href:"https://web.pulsar-edit.dev",target:"_blank",rel:"noopener noreferrer"},m={href:"https://github.com",target:"_blank",rel:"noopener noreferrer"},b=n('When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\\d+\\.\\d+\\.\\d+
^v\\d+\\.\\d+\\.\\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
pulsar -p publish --tag tagname
where tagname
must match the name of the tag created in the above step$ pulsar -p unpublish <package-name>
+
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ pulsar -p unpublish <package-name@1.2.3>
+
If you need to rename your package for any reason, you can do so with one simple command \u2013 pulsar -p publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ pulsar -p publish --rename <new-package-name>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press Cmd+RCtrl+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to Cmd+TCtrl+T. You can also search for symbols across your project but it requires a tags
file.
Atom also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press Cmd+F2Alt+Ctrl+F2Ctrl+Shift+F2, Atom will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Atom will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
',6),ne={href:"https://github.com/atom/bookmarks",target:"_blank",rel:"noopener noreferrer"};function ae(se,ie){const r=d("Tabs"),m=d("RouterLink"),l=d("ExternalLinkIcon");return h(),f("div",null,[v,k,i(r,{id:"6",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:t(({title:n,value:a,isActive:s})=>[y,_]),tab1:t(({title:n,value:a,isActive:s})=>[w,x]),tab2:t(({title:n,value:a,isActive:s})=>[C,A]),_:1}),T,e("p",null,[o("Atom also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the "),i(m,{to:"/getting-started/sections/atom-basics/#command-palette"},{default:t(()=>[o("Command Palette")]),_:1}),o(", but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your "),j,o(" to create a key combination. You can open "),S,o(" file in an editor from the "),F,L,M,o(" menu.")]),R,i(r,{id:"79",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:t(({title:n,value:a,isActive:s})=>[E]),tab1:t(({title:n,value:a,isActive:s})=>[Y]),tab2:t(({title:n,value:a,isActive:s})=>[I]),_:1}),e("p",null,[o("This will bind the command "),q,o(" to "),z,V,o(". For more information on customizing your keybindings, see "),i(m,{to:"/using-atom/sections/basic-customization/#customizing-keybindings"},{default:t(()=>[o("Customizing Keybindings")]),_:1}),o(".")]),B,i(r,{id:"96",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:t(({title:n,value:a,isActive:s})=>[N]),tab1:t(({title:n,value:a,isActive:s})=>[W]),tab2:t(({title:n,value:a,isActive:s})=>[O]),_:1}),K,e("p",null,[o("You can generate a "),U,o(" file by using the "),e("a",D,[o("ctags utility"),i(l)]),o(". Once it is installed, you can use it to generate a "),G,o(" file by running a command to generate it. See the "),e("a",H,[o("ctags documentation"),i(l)]),o(" for details.")]),i(r,{id:"119",data:[{title:"Linux"},{title:"Mac"},{title:"Windows"}],"tab-id":"moving-in-atom"},{tab0:t(({title:n,value:a,isActive:s})=>[P]),tab1:t(({title:n,value:a,isActive:s})=>[J]),tab2:t(({title:n,value:a,isActive:s})=>[Q]),_:1}),e("p",null,[o("You can customize how tags are generated by creating your own "),X,o(" file in your home directory, "),Z,$,o(". An example can be found "),e("a",ee,[o("here"),i(l)]),o(".")]),e("p",null,[o("The symbols navigation functionality is implemented in the "),e("a",oe,[o("symbols-view"),i(l)]),o(" package.")]),te,o(' ![View and filter bookmarks](@images/atom/bookmarks.png "View and filter bookmarks") '),e("p",null,[o("The bookmarks functionality is implemented in the "),e("a",ne,[o("bookmarks"),i(l)]),o(" package.")])])}const de=p(g,[["render",ae],["__file","moving-in-atom.html.vue"]]);export{de as default}; diff --git a/assets/moving-in-pulsar.html.325aabf8.js b/assets/moving-in-pulsar.html.325aabf8.js new file mode 100644 index 0000000000..f2049947dc --- /dev/null +++ b/assets/moving-in-pulsar.html.325aabf8.js @@ -0,0 +1 @@ +const n=JSON.parse('{"key":"v-15cc7078","path":"/docs/launch-manual/sections/using-pulsar/sections/moving-in-pulsar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Moving in Pulsar","slug":"moving-in-pulsar","link":"#moving-in-pulsar","children":[{"level":3,"title":"Additional Movement and Selection Commands","slug":"additional-movement-and-selection-commands","link":"#additional-movement-and-selection-commands","children":[]},{"level":3,"title":"Navigating by Symbols","slug":"navigating-by-symbols","link":"#navigating-by-symbols","children":[]},{"level":3,"title":"Bookmarks","slug":"bookmarks","link":"#bookmarks","children":[]}]}],"git":{"updatedTime":1669229414000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":4}]},"readingTime":{"minutes":5.31,"words":1594},"filePathRelative":"docs/launch-manual/sections/using-pulsar/sections/moving-in-pulsar.md"}');export{n as data}; diff --git a/assets/moving-in-pulsar.html.c8a1d28a.js b/assets/moving-in-pulsar.html.c8a1d28a.js new file mode 100644 index 0000000000..3c60c0db44 --- /dev/null +++ b/assets/moving-in-pulsar.html.c8a1d28a.js @@ -0,0 +1,52 @@ +import{_ as u,a as m}from"./symbol.b88bf5a9.js";import{_ as b}from"./bookmarks.736c4e14.js";import{_ as h,o as g,c as p,d as r,w as o,a as e,b as t,f as d,r as c}from"./app.0e1565ce.js";const v={},f=e("h2",{id:"moving-in-pulsar",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#moving-in-pulsar","aria-hidden":"true"},"#"),t(" Moving in Pulsar")],-1),y=e("p",null,"While it's pretty easy to move around Pulsar by clicking with the mouse or using the arrow keys, there are some keybindings that may help you keep your hands on the keyboard and navigate around a little faster.",-1),k=e("p",null,"Pulsar has support for all the standard Linux cursor movement key combinations. To go up, down, left or right a single character you can use the arrow keys.",-1),_=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),w=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Left"),t(" - Move to the beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Right"),t(" - Move to the end of word")]),e("li",null,[e("kbd",null,"Home"),t(" - Move to the first character of the current line")]),e("li",null,[e("kbd",null,"End"),t(" - Move to the end of the line")]),e("li",null,[e("kbd",null,"Ctrl+Home"),t(" - Move to the top of the file")]),e("li",null,[e("kbd",null,"Ctrl+End"),t(" - Move to the bottom of the file")])],-1),x=e("p",null,[t("Pulsar ships with many of the basic Emacs keybindings for navigating a document. To go up and down a single character, you can use "),e("kbd",null,"Ctrl+P"),t(" and "),e("kbd",null,"Ctrl+N"),t(". To go left and right a single character, you can use "),e("kbd",null,"Ctrl+B"),t(" and "),e("kbd",null,"Ctrl+F"),t(". These are the equivalent of using the arrow keys, though some people prefer not having to move their hands to where the arrow keys are located on their keyboard.")],-1),C=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),A=e("ul",null,[e("li",null,[e("kbd",null,"Alt+Left"),t(" or "),e("kbd",null,"Alt+B"),t(" - Move to the beginning of word")]),e("li",null,[e("kbd",null,"Alt+Right"),t(" or "),e("kbd",null,"Alt+F"),t(" - Move to the end of word")]),e("li",null,[e("kbd",null,"Cmd+Left"),t(" or "),e("kbd",null,"Ctrl+A"),t(" - Move to the first character of the current line")]),e("li",null,[e("kbd",null,"Cmd+Right"),t(" or "),e("kbd",null,"Ctrl+E"),t(" - Move to the end of the line")]),e("li",null,[e("kbd",null,"Cmd+Up"),t(" - Move to the top of the file")]),e("li",null,[e("kbd",null,"Cmd+Down"),t(" - Move to the bottom of the file")])],-1),M=e("p",null,"Pulsar has support for all the standard Windows cursor movement key combinations. To go up, down, left or right a single character you can use the arrow keys.",-1),T=e("p",null,"In addition to single character movement, there are a number of other movement keybindings:",-1),L=e("ul",null,[e("li",null,[e("kbd",null,"Ctrl+Left"),t(" - Move to the beginning of word")]),e("li",null,[e("kbd",null,"Ctrl+Right"),t(" - Move to the end of word")]),e("li",null,[e("kbd",null,"Home"),t(" - Move to the first character of the current line")]),e("li",null,[e("kbd",null,"End"),t(" - Move to the end of the line")]),e("li",null,[e("kbd",null,"Ctrl+Home"),t(" - Move to the top of the file")]),e("li",null,[e("kbd",null,"Ctrl+End"),t(" - Move to the bottom of the file")])],-1),N=d('You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
Pulsar also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the Command Palette, but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your keymap.cson
to create a key combination. You can open keymap.cson
file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu. For example, the command editor:move-to-beginning-of-screen-line
is available in the command palette, but it's not bound to any key combination. To create a key combination you need to add an entry in your keymap.cson
file. For editor:select-to-previous-word-boundary
, you can add the following to your keymap.cson
:
This will bind the command editor:select-to-previous-word-boundary
to LNX/WIN: Ctrl+Shift+E - MAC: Cmd+Shift+E. For more information on customizing your keybindings, see Customizing Keybindings.
Here's a list of Movement and Selection Commands that do not have a keyboard shortcut by default:
',2),P=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`editor:move-to-beginning-of-next-paragraph +editor:move-to-beginning-of-previous-paragraph +editor:move-to-beginning-of-screen-line +editor:move-to-beginning-of-line +editor:move-to-end-of-line +editor:move-to-first-character-of-line +editor:move-to-beginning-of-next-word +editor:move-to-previous-word-boundary +editor:move-to-next-word-boundary +editor:select-to-beginning-of-next-paragraph +editor:select-to-beginning-of-previous-paragraph +editor:select-to-end-of-line +editor:select-to-beginning-of-line +editor:select-to-beginning-of-next-word +editor:select-to-next-word-boundary +editor:select-to-previous-word-boundary +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),F=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`editor:move-to-beginning-of-next-paragraph +editor:move-to-beginning-of-previous-paragraph +editor:move-to-beginning-of-screen-line +editor:move-to-beginning-of-line +editor:move-to-beginning-of-next-word +editor:move-to-previous-word-boundary +editor:move-to-next-word-boundary +editor:select-to-beginning-of-next-paragraph +editor:select-to-beginning-of-previous-paragraph +editor:select-to-beginning-of-line +editor:select-to-beginning-of-next-word +editor:select-to-next-word-boundary +editor:select-to-previous-word-boundary +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),W=e("div",{class:"language-text ext-text line-numbers-mode"},[e("pre",{class:"language-text"},[e("code",null,`editor:move-to-beginning-of-next-paragraph +editor:move-to-beginning-of-previous-paragraph +editor:move-to-beginning-of-screen-line +editor:move-to-beginning-of-line +editor:move-to-end-of-line +editor:move-to-first-character-of-line +editor:move-to-beginning-of-next-word +editor:move-to-previous-word-boundary +editor:move-to-next-word-boundary +editor:select-to-beginning-of-next-paragraph +editor:select-to-beginning-of-previous-paragraph +editor:select-to-end-of-line +editor:select-to-beginning-of-line +editor:select-to-beginning-of-next-word +editor:select-to-next-word-boundary +editor:select-to-previous-word-boundary +`)]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),O=d('You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press LNX/WIN: Ctrl+R - MAC: Cmd+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to LNX/WIN: Ctrl+T - MAC: Cmd+T. You can also search for symbols across your project but it requires a tags
file.
Pulsar also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press LNX/WIN: Alt+Ctrl+F2 - MAC Cmd+F2, Pulsar will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Pulsar will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Pulsar. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Pulsar will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Pulsar uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Pulsar for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Pulsar to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the Active Editor Info: Toggle
command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = \`
+ <h2>\${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> \${item.softWrapped}</li>
+ <li><b>Tab Length:</b> \${item.getTabLength()}</li>
+ <li><b>Encoding:</b> \${item.getEncoding()}</li>
+ <li><b>Line Count:</b> \${item.getLineCount()}</li>
+ </ul>
+ \`;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Pulsar how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Pulsar and toggle the view with the Active Editor Info: Toggle
command. Then reload Pulsar again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
`,36);function _(I,x){const a=l("ExternalLinkIcon");return i(),p("div",null,[r,d,t("Below was part of this section in the original docs but as Nuclide is retired we either need a new example or remove this entirely"),t(`This system is used by Pulsar's tree view, as well as by third party +packages like [Nuclide](https://nuclide.io) for its console, debugger, outline +view, and diagnostics (linter results).`),n("p",null,[s("For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at "),n("a",k,[s("https://github.com/pulsar-edit/active-editor-info"),e(a)]),s(".")]),v,n("p",null,[s("To begin, press "),m,s(": "),h,s(" - "),g,s(": "),b,s(" to bring up the "),n("a",w,[s("Command Palette"),e(a)]),s('. Type "generate package" and select the '),f,s(" command, just as we did in "),y,s(". Enter "),q,s(" as the name of the package.")]),j])}const T=o(u,[["render",_],["__file","package-active-editor-info.html.vue"]]);export{T as default}; diff --git a/assets/package-active-editor-info.html.765e6517.js b/assets/package-active-editor-info.html.765e6517.js new file mode 100644 index 0000000000..6225a8403f --- /dev/null +++ b/assets/package-active-editor-info.html.765e6517.js @@ -0,0 +1,102 @@ +import{_ as p,o as c,c as l,a as s,b as n,d as a,w as o,f as u,r as i}from"./app.0e1565ce.js";const r={},d=s("h3",{id:"package-active-editor-info",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#package-active-editor-info","aria-hidden":"true"},"#"),n(" Package: Active Editor Info")],-1),k={href:"https://nuclide.io",target:"_blank",rel:"noopener noreferrer"},v=s("p",null,"For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at https://github.com/atom/active-editor-info.",-1),m=s("h4",{id:"create-the-package",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#create-the-package","aria-hidden":"true"},"#"),n(" Create the Package")],-1),h=s("kbd",{class:"platform-mac"},"Cmd+Shift+P",-1),g=s("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+P",-1),b={href:"https://github.com/atom/command-palette",target:"_blank",rel:"noopener noreferrer"},w=s("code",null,"active-editor-info",-1),f=u(`Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Atom. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Atom will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Atom uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Atom for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Atom to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the "Active Editor Info: Toggle" command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = \`
+ <h2>\${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> \${item.softWrapped}</li>
+ <li><b>Tab Length:</b> \${item.getTabLength()}</li>
+ <li><b>Encoding:</b> \${item.getEncoding()}</li>
+ <li><b>Line Count:</b> \${item.getLineCount()}</li>
+ </ul>
+ \`;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Atom how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Atom and toggle the view with the "Active Editor Info: Toggle" command. Then reload Atom again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
`,36);function y(q,A){const t=i("RouterLink"),e=i("ExternalLinkIcon");return c(),l("div",null,[d,s("p",null,[n("We saw in our "),a(t,{to:"/hacking-atom/sections/package-word-count/"},{default:o(()=>[n("Word Count")]),_:1}),n(" package how we could show information in a modal panel. However, panels aren't the only way to extend Atom's UI\u2014you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Atom will restore them the next time you open the project. This system is used by Atom's tree view, as well as by third party packages like "),s("a",k,[n("Nuclide"),a(e)]),n(" for its console, debugger, outline view, and diagnostics (linter results).")]),v,m,s("p",null,[n("To begin, press "),h,g,n(" to bring up the "),s("a",b,[n("Command Palette"),a(e)]),n('. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in '),a(t,{to:"/hacking-atom/sections/package-word-count/#package-generator"},{default:o(()=>[n("the section on package generation")]),_:1}),n(". Enter "),w,n(" as the name of the package.")]),f])}const x=p(r,[["render",y],["__file","package-active-editor-info.html.vue"]]);export{x as default}; diff --git a/assets/package-active-editor-info.html.f8b84ee7.js b/assets/package-active-editor-info.html.f8b84ee7.js new file mode 100644 index 0000000000..f6240d96dd --- /dev/null +++ b/assets/package-active-editor-info.html.f8b84ee7.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-878fda62","path":"/docs/launch-manual/sections/core-hacking/sections/package-active-editor-info.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Package: Active Editor Info","slug":"package-active-editor-info","link":"#package-active-editor-info","children":[{"level":3,"title":"Create the Package","slug":"create-the-package","link":"#create-the-package","children":[]},{"level":3,"title":"Add an Opener","slug":"add-an-opener","link":"#add-an-opener","children":[]},{"level":3,"title":"Updating the View","slug":"updating-the-view","link":"#updating-the-view","children":[]},{"level":3,"title":"Constraining Our Item's Locations","slug":"constraining-our-item-s-locations","link":"#constraining-our-item-s-locations","children":[]},{"level":3,"title":"Show Active Editor Info","slug":"show-active-editor-info","link":"#show-active-editor-info","children":[]},{"level":3,"title":"Serialization","slug":"serialization","link":"#serialization","children":[]},{"level":3,"title":"Summary","slug":"summary","link":"#summary","children":[]}]}],"git":{"updatedTime":1670466847000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":4.45,"words":1335},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/package-active-editor-info.md"}`);export{e as data}; diff --git a/assets/package-frontend.a8bc39dd.png b/assets/package-frontend.a8bc39dd.png new file mode 100644 index 0000000000..8a7310b10e Binary files /dev/null and b/assets/package-frontend.a8bc39dd.png differ diff --git a/assets/package-issue-link.51bb6d85.js b/assets/package-issue-link.51bb6d85.js new file mode 100644 index 0000000000..8a7921ae82 --- /dev/null +++ b/assets/package-issue-link.51bb6d85.js @@ -0,0 +1 @@ +const s="/assets/package-issue-link.65162d65.png";export{s as _}; diff --git a/assets/package-issue-link.65162d65.png b/assets/package-issue-link.65162d65.png new file mode 100644 index 0000000000..863533d0dd Binary files /dev/null and b/assets/package-issue-link.65162d65.png differ diff --git a/assets/package-modifying-text.html.02ee0996.js b/assets/package-modifying-text.html.02ee0996.js new file mode 100644 index 0000000000..b9480a6f17 --- /dev/null +++ b/assets/package-modifying-text.html.02ee0996.js @@ -0,0 +1,63 @@ +import{_ as p,o as l,c as r,a,b as n,d as s,w as i,f as t,r as c}from"./app.0e1565ce.js";const d={},u=a("h3",{id:"package-modifying-text",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#package-modifying-text","aria-hidden":"true"},"#"),n(" Package: Modifying Text")],-1),k={href:"https://en.wikipedia.org/wiki/ASCII_art",target:"_blank",rel:"noopener noreferrer"},h=t(` o888
+ ooooooo ooooooo ooooooo 888
+ 888 888 888 888 888 888 888
+ 888 888 888 888 888 888
+ 88ooo888 88ooo88 88ooo88 o888o
+
+
This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.
The final package can be viewed at https://github.com/atom/ascii-art.
Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing Alt+Cmd+Ctrl+LCtrl+Shift+F5.
Now open the Command Palette and search for the "Ascii Art: Convert" command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Atom launch faster by allowing Atom to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the "Ascii Art: Convert" command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
Warning: The Atom keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command "Update Package Dependencies: Update" from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run "Update Package Dependencies: Update" whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(\`\\n\${art}\\n\`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
`,6),I={href:"https://atom.io/docs/api/latest/TextEditor#instance-getSelectedText",target:"_blank",rel:"noopener noreferrer"},S=a("code",null,"editor.getSelectedText()",-1),N={href:"https://atom.io/docs/api/latest/TextEditor#instance-insertText",target:"_blank",rel:"noopener noreferrer"},W=a("code",null,"editor.insertText()",-1),P=a("h4",{id:"summary",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#summary","aria-hidden":"true"},"#"),n(" Summary")],-1),E=a("p",null,"In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.",-1);function B(R,L){const e=c("ExternalLinkIcon"),o=c("RouterLink");return l(),r("div",null,[u,a("p",null,[n("Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with "),a("a",k,[n("ascii art"),s(e)]),n('. When you run our new command with the word "cool" selected, it will be replaced with:')]),h,a("p",null,[n("To begin, press "),m,v,n(" to bring up the "),a("a",g,[n("Command Palette"),s(e)]),n('. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in '),s(o,{to:"/hacking-atom/sections/package-word-count/#package-generator"},{default:i(()=>[n("the section on package generation")]),_:1}),n(". Enter "),b,n(" as the name of the package.")]),f,a("p",null,[n("As in "),s(o,{to:"/hacking-atom/sections/package-word-count/#counting-the-words"},{default:i(()=>[n("Counting Words")]),_:1}),n(", we're using "),w,n(" to get the object that represents the active text editor. If this "),y,n(" method is called when not focused on a text editor, nothing will happen.")]),a("p",null,[n("Next we insert a string into the current text editor with the "),a("a",x,[_,s(e)]),n(' method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.')]),q,a("p",null,[n("Now we need to convert the selected text to ASCII art. To do this we will use the "),a("a",A,[n("figlet"),s(e)]),n(" Node module from "),a("a",T,[n("npm"),s(e)]),n(". Open "),C,n(" and add the latest version of figlet to the dependencies:")]),j,a("p",null,[n("There are a couple of new things in this example we should look at quickly. The first is the "),a("a",I,[S,s(e)]),n(" which, as you might guess, returns the text that is currently selected.")]),a("p",null,[n("We then call the Figlet code to convert that into something else and replace the current selection with it with the "),a("a",N,[W,s(e)]),n(" call.")]),P,E])}const D=p(d,[["render",B],["__file","package-modifying-text.html.vue"]]);export{D as default}; diff --git a/assets/package-modifying-text.html.8104c421.js b/assets/package-modifying-text.html.8104c421.js new file mode 100644 index 0000000000..3b1a3b9305 --- /dev/null +++ b/assets/package-modifying-text.html.8104c421.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-e59f4bf8","path":"/docs/launch-manual/sections/core-hacking/sections/package-modifying-text.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Package: Modifying Text","slug":"package-modifying-text","link":"#package-modifying-text","children":[{"level":3,"title":"Basic Text Insertion","slug":"basic-text-insertion","link":"#basic-text-insertion","children":[]},{"level":3,"title":"Add the ASCII Art","slug":"add-the-ascii-art","link":"#add-the-ascii-art","children":[]},{"level":3,"title":"Summary","slug":"summary","link":"#summary","children":[]}]}],"git":{"updatedTime":1670466847000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":3.83,"words":1149},"filePathRelative":"docs/launch-manual/sections/core-hacking/sections/package-modifying-text.md"}');export{e as data}; diff --git a/assets/package-modifying-text.html.a1bdfaa6.js b/assets/package-modifying-text.html.a1bdfaa6.js new file mode 100644 index 0000000000..da94a6b85e --- /dev/null +++ b/assets/package-modifying-text.html.a1bdfaa6.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-4284f843","path":"/docs/atom-archive/hacking-atom/sections/package-modifying-text.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Package: Modifying Text","slug":"package-modifying-text","link":"#package-modifying-text","children":[]}],"git":{"updatedTime":1664050274000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":3.89,"words":1166},"filePathRelative":"docs/atom-archive/hacking-atom/sections/package-modifying-text.md"}');export{e as data}; diff --git a/assets/package-modifying-text.html.ca206c57.js b/assets/package-modifying-text.html.ca206c57.js new file mode 100644 index 0000000000..3c5c743cc5 --- /dev/null +++ b/assets/package-modifying-text.html.ca206c57.js @@ -0,0 +1,63 @@ +import{_ as i,o as c,c as p,a as n,b as e,d as a,e as t,f as o,r as l}from"./app.0e1565ce.js";const r={},d=n("h2",{id:"package-modifying-text",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#package-modifying-text","aria-hidden":"true"},"#"),e(" Package: Modifying Text")],-1),u={href:"https://en.wikipedia.org/wiki/ASCII_art",target:"_blank",rel:"noopener noreferrer"},k=n("pre",null,` o888 + ooooooo ooooooo ooooooo 888 + 888 888 888 888 888 888 888 + 888 888 888 888 888 888 + 88ooo888 88ooo88 88ooo88 o888o + +`,-1),h=n("p",null,"This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.",-1),m=n("p",null,"The final package can be viewed at https://github.com/pulsar-edit/ascii-art.",-1),v=n("h3",{id:"basic-text-insertion",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#basic-text-insertion","aria-hidden":"true"},"#"),e(" Basic Text Insertion")],-1),g=n("strong",null,[n("em",null,"LNX/WIN")],-1),b=n("kbd",null,"Ctrl+Shift+P",-1),w=n("strong",null,[n("em",null,"MAC")],-1),f=n("kbd",null,"Cmd+Shift+P",-1),y={href:"https://github.com/pulsar-edit/command-palette",target:"_blank",rel:"noopener noreferrer"},_=n("a",{href:"#package-generator"},"the section on package generation",-1),x=n("code",null,"ascii-art",-1),q=o(`Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
As in Counting Words, we're using atom.workspace.getActiveTextEditor()
to get the object that represents the active text editor. If this convert()
method is called when not focused on a text editor, nothing will happen.
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L
Now open the Command Palette and search for the Ascii Art: Convert
command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Pulsar launch faster by allowing Pulsar to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the Ascii Art: Convert
command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
The Pulsar keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command Update Package Dependencies: Update
from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run Update Package Dependencies: Update
whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(\`\\n\${art}\\n\`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
`,6),P={href:"https://atom.io/docs/api/latest/TextEditor#instance-getSelectedText",target:"_blank",rel:"noopener noreferrer"},W=n("code",null,"editor.getSelectedText()",-1),E={href:"https://atom.io/docs/api/latest/TextEditor#instance-insertText",target:"_blank",rel:"noopener noreferrer"},B=n("code",null,"editor.insertText()",-1),O=n("h3",{id:"summary",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#summary","aria-hidden":"true"},"#"),e(" Summary")],-1),R=n("p",null,"In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.",-1);function D(L,U){const s=l("ExternalLinkIcon");return c(),p("div",null,[d,n("p",null,[e("Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with "),n("a",u,[e("ascii art"),a(s)]),e('. When you run our new command with the word "cool" selected, it will be replaced with:')]),k,h,m,v,n("p",null,[e("To begin, press "),g,e(": "),b,e(" - "),w,e(": "),f,e(" to bring up the "),n("a",y,[e("Command Palette"),a(s)]),e('. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in '),_,e(". Enter "),x,e(" as the name of the package.")]),q,n("p",null,[e("Next we insert a string into the current text editor with the "),n("a",T,[A,a(s)]),t("TODO: Replace with API link once documented)"),e(' method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.')]),C,n("p",null,[e("Now we need to convert the selected text to ASCII art. To do this we will use the "),n("a",j,[e("figlet"),a(s)]),e(" Node module from "),n("a",I,[e("npm"),a(s)]),e(". Open "),N,e(" and add the latest version of figlet to the dependencies:")]),S,n("p",null,[e("There are a couple of new things in this example we should look at quickly. The first is the "),n("a",P,[W,a(s)]),t("TODO: Replace with API link once documented)"),e(" which, as you might guess, returns the text that is currently selected.")]),n("p",null,[e("We then call the Figlet code to convert that into something else and replace the current selection with it with the "),n("a",E,[B,a(s)]),t("TODO: Replace with API link once documented)"),e(" call.")]),O,R])}const F=i(r,[["render",D],["__file","package-modifying-text.html.vue"]]);export{F as default}; diff --git a/assets/package-settings.ad8a3253.png b/assets/package-settings.ad8a3253.png new file mode 100644 index 0000000000..828653355d Binary files /dev/null and b/assets/package-settings.ad8a3253.png differ diff --git a/assets/package-specific-settings.970b531c.png b/assets/package-specific-settings.970b531c.png new file mode 100644 index 0000000000..3a19db7ecb Binary files /dev/null and b/assets/package-specific-settings.970b531c.png differ diff --git a/assets/package-webhook.7e0f3c29.png b/assets/package-webhook.7e0f3c29.png new file mode 100644 index 0000000000..37a660c8c7 Binary files /dev/null and b/assets/package-webhook.7e0f3c29.png differ diff --git a/assets/package-word-count.html.0ef1249a.js b/assets/package-word-count.html.0ef1249a.js new file mode 100644 index 0000000000..5f25acecab --- /dev/null +++ b/assets/package-word-count.html.0ef1249a.js @@ -0,0 +1,212 @@ +import{_ as c,a as l,b as u,c as r,d,e as k}from"./spec-suite.8f5e854d.js";import{_ as m}from"./dev-tools.1b7b2813.js";import{_ as h,o as v,c as g,a as n,b as s,d as a,w as p,f as t,r as i}from"./app.0e1565ce.js";const y={},b=n("h3",{id:"package-word-count",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#package-word-count","aria-hidden":"true"},"#"),s(" Package: Word Count")],-1),w=n("p",null,"Let's get started by writing a very simple package and looking at some of the tools needed to develop one effectively. We'll start by writing a package that tells you how many words are in the current buffer and display it in a small modal window.",-1),f=n("h4",{id:"package-generator",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#package-generator","aria-hidden":"true"},"#"),s(" Package Generator")],-1),q={href:"https://github.com/atom/package-generator",target:"_blank",rel:"noopener noreferrer"},_=n("p",null,[s('You can run the generator by invoking the command palette and searching for "Generate Package". A dialog will appear asking you to name your new project. Name it '),n("code",null,"your-name-word-count"),s(". Atom will then create that directory and fill it out with a skeleton project and link it into your "),n("span",{class:"platform-mac platform-linux"},[n("code",null,"~/.atom/packages")]),n("span",{class:"platform-windows"},[n("code",null,"%USERPROFILE%\\.atom\\packages")]),s(" directory so it's loaded when you launch your editor next time.")],-1),x={class:"custom-container note"},j=n("p",{class:"custom-container-title"},"Note",-1),C=n("strong",null,"Note:",-1),T={href:"https://atom.io/packages",target:"_blank",rel:"noopener noreferrer"},A=n("code",null,"your-name-word-count",-1),I=n("em",null,"should",-1),W=t('You can see that Atom has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+\u251C\u2500 grammars/
+\u251C\u2500 keymaps/
+\u251C\u2500 lib/
+\u251C\u2500 menus/
+\u251C\u2500 spec/
+\u251C\u2500 snippets/
+\u251C\u2500 styles/
+\u251C\u2500 index.js
+\u2514\u2500 package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Atom will default to looking for an index.coffee
or index.js
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Atom has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Warning: Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Atom's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Atom. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
`,11),E=n("li",null,[n("code",null,"activate(state)"),s(": This "),n("strong",null,"optional"),s(" method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the "),n("code",null,"serialize()"),s(" method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).")],-1),Y=n("code",null,"initialize(state)",-1),L=n("strong",null,"optional",-1),O=n("code",null,"activate()",-1),M=n("code",null,"initialize()",-1),R=n("code",null,"activate()",-1),U=n("code",null,"initialize()",-1),B=n("li",null,[n("code",null,"serialize()"),s(": This "),n("strong",null,"optional"),s(" method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's "),n("code",null,"activate"),s(" method so you can restore your view to where the user left off.")],-1),F=n("li",null,[n("code",null,"deactivate()"),s(": This "),n("strong",null,"optional"),s(" method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.")],-1),G=n("h5",{id:"style-sheets",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#style-sheets","aria-hidden":"true"},"#"),s(" Style Sheets")],-1),K=n("code",null,"styles",-1),H={href:"http://lesscss.org",target:"_blank",rel:"noopener noreferrer"},J=n("p",null,[s("Ideally, you won't need much in the way of styling. Atom provides a standard set of components which define both the colors and UI elements for any package that fits into Atom seamlessly. You can view all of Atom's UI components by opening the styleguide: open the command palette "),n("kbd",{class:"platform-mac"},"Cmd+Shift+P"),n("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+P"),s(" and search for "),n("code",null,"styleguide"),s(", or type "),n("kbd",{class:"platform-mac"},"Cmd+Ctrl+Shift+G"),n("kbd",{class:"platform-windows platform-linux"},"Ctrl+Shift+G"),s(".")],-1),$=n("em",null,"do",-1),Q=n("em",null,"must",-1),X={href:"https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less",target:"_blank",rel:"noopener noreferrer"},Z=t(`An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Atom UI, this means the key combination will work anywhere in the application.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your plugin.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Atom text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole plugin.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Atom but optional. The toggle
method is one Atom is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Atom starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Atom workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple plugin. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the plugin so other plugins could subscribe to these events.
// Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Tip: Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = \`There are \${count} words.\`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
Note: To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Atom windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Atom being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press Alt+Cmd+ICtrl+Shift+I, or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Atom.
Once you've got your test suite written, you can run it by pressing Alt+Cmd+Ctrl+PAlt+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the atom --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
You can see that Pulsar has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+\u251C\u2500 grammars/
+\u251C\u2500 keymaps/
+\u251C\u2500 lib/
+\u251C\u2500 menus/
+\u251C\u2500 spec/
+\u251C\u2500 snippets/
+\u251C\u2500 styles/
+\u251C\u2500 index.js
+\u2514\u2500 package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Pulsar will default to looking for an index.js
or index.coffee
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Pulsar has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Pulsar's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Pulsar. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
activate(state)
: This optional method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the serialize()
method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).initialize(state)
: This optional method is similar to activate()
but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after your package's deserializers have been called), initialize()
is guaranteed to be called before everything. Use activate()
if you want to be sure that the workspace is ready; use initialize()
if you need to do some setup prior to your deserializers or view providers being invoked.serialize()
: This optional method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's activate
method so you can restore your view to where the user left off.deactivate()
: This optional method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.Ideally, you won't need much in the way of styling. Pulsar provides a standard set of components which define both the colors and UI elements for any package that fits into Pulsar seamlessly. You can view all of Pulsar's UI components by opening the styleguide: open the command palette LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or type LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G
An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Pulsar UI, this means the key combination will work anywhere in the application.
We'll cover more advanced keybinding stuff a bit later in Keymaps in Depth.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your package.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Pulsar text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole package.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Pulsar but optional. The toggle
method is one Pulsar is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Pulsar starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Pulsar workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple package. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the package so other packages could subscribe to these events.
// Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = \`There are \${count} words.\`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Pulsar windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Pulsar being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Pulsar.
Once you've got your test suite written, you can run it by pressing LNX/WIN: Alt+Ctrl+P - MAC: Alt+Cmd+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the pulsar --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
We've now generated, customized and tested our first package for Pulsar. Congratulations! Now let's go ahead and publish it so it's available to the world.
',6);function H(J,$){const a=v("ExternalLinkIcon");return m(),h("div",null,[y,b,w,n("p",null,[s("The simplest way to start a package is to use the built-in package generator that ships with Pulsar. As you might expect by now, this generator is itself a separate package implemented in "),n("a",f,[s("package-generator"),e(a)]),s(".")]),q,n("div",x,[_,n("p",null,[s("You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on "),n("a",j,[s("pulsar-edit.dev"),e(a)]),s(' (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the '),C,s(" package name, you "),T,s(" be safe \u{1F600}")])]),P,n("p",null,[s("Similar to "),n("a",I,[s("Node modules"),e(a)]),s(", Pulsar packages contain a "),N,s(' file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.')]),n("p",null,[s("In addition to some of the regular "),n("a",W,[s("Node "),A,s(" keys"),e(a)]),s(" available, Pulsar "),S,s(" files have their own additions.")]),V,n("p",null,[s("Style sheets for your package should be placed in the "),D,s(" directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or "),n("a",E,[s("Less"),e(a)]),s(", but Less is recommended.")]),L,n("p",null,[s("If you "),z,s(" need special styling, try to keep only structural styles in the package style sheets. If you "),Y,s(" specify colors and sizing, these should be taken from the active theme's "),n("a",O,[s("ui-variables.less"),e(a)]),s(".")]),M,n("p",null,[s("Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling "),n("a",U,[R,e(a)]),s(". "),o("TODO: Update when API is documented")]),n("p",null,[s("Next we get the number of words by calling "),n("a",B,[F,e(a)]),o("TODO: Update when API is documented"),s(" on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.")]),G,n("p",null,[s("Under the hood, "),n("a",K,[s("Jasmine v1.3"),e(a)]),s(" executes your tests, so you can assume that any DSL available there is also available to your package.")]),X])}const sn=k(g,[["render",H],["__file","package-word-count.html.vue"]]);export{sn as default}; diff --git a/assets/package.055df86c.png b/assets/package.055df86c.png new file mode 100644 index 0000000000..ab89679191 Binary files /dev/null and b/assets/package.055df86c.png differ diff --git a/assets/package.4e7449ae.js b/assets/package.4e7449ae.js new file mode 100644 index 0000000000..5c666f5414 --- /dev/null +++ b/assets/package.4e7449ae.js @@ -0,0 +1 @@ +const s="/assets/package.d325e49c.png";export{s as _}; diff --git a/assets/package.d325e49c.png b/assets/package.d325e49c.png new file mode 100644 index 0000000000..4580b69b8f Binary files /dev/null and b/assets/package.d325e49c.png differ diff --git a/assets/packages-install.5546715f.png b/assets/packages-install.5546715f.png new file mode 100644 index 0000000000..ba036413c0 Binary files /dev/null and b/assets/packages-install.5546715f.png differ diff --git a/assets/panes.a7cec271.js b/assets/panes.a7cec271.js new file mode 100644 index 0000000000..bd065cb688 --- /dev/null +++ b/assets/panes.a7cec271.js @@ -0,0 +1 @@ +const s="/assets/panes.bea7c577.png";export{s as _}; diff --git a/assets/panes.bea7c577.png b/assets/panes.bea7c577.png new file mode 100644 index 0000000000..11a99696f1 Binary files /dev/null and b/assets/panes.bea7c577.png differ diff --git a/assets/panes.html.4b274c2a.js b/assets/panes.html.4b274c2a.js new file mode 100644 index 0000000000..e87ce0f794 --- /dev/null +++ b/assets/panes.html.4b274c2a.js @@ -0,0 +1 @@ +import{_ as o}from"./panes.a7cec271.js";import{_ as n,o as s,c as i,a as e,b as t,d as l,f as r,r as c}from"./app.0e1565ce.js";const p={},d=r('You can split any editor pane horizontally or vertically by using Cmd+KCtrl+K Up/Down/Left/Right where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with Cmd+KCtrl+K Cmd+Up/Down/Left/RightCtrl+Up/Down/Left/Right where the direction is the direction the focus should move to.
Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.
',4),h={class:"custom-container tip"},m=e("p",{class:"custom-container-title"},"Tip",-1),u={href:"https://github.com/atom/tabs",target:"_blank",rel:"noopener noreferrer"},f=e("p",null,[t("To close a pane, you can close all pane items with "),e("kbd",{class:"platform-mac"},"Cmd+W"),e("kbd",{class:"platform-windows platform-linux"},"Ctrl+W"),t('. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.')],-1);function b(w,_){const a=c("ExternalLinkIcon");return s(),i("div",null,[d,e("div",h,[m,e("p",null,[t("If you don't like using tabs, you don't have to. You can disable the "),e("a",u,[t("tabs package"),l(a)]),t(" and each pane will still support multiple pane items. You just won't have tabs to use to click between them.")])]),f])}const y=n(p,[["render",b],["__file","panes.html.vue"]]);export{y as default}; diff --git a/assets/panes.html.690697a2.js b/assets/panes.html.690697a2.js new file mode 100644 index 0000000000..9faacf7945 --- /dev/null +++ b/assets/panes.html.690697a2.js @@ -0,0 +1 @@ +import{_ as i}from"./panes.a7cec271.js";import{_ as r,o as u,c as d,d as l,w as n,a as e,b as t,r as h}from"./app.0e1565ce.js";const m={},w=e("h2",{id:"panes",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#panes","aria-hidden":"true"},"#"),t(" Panes")],-1),_=e("p",null,[t("You can split any editor pane horizontally or vertically by using "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Up/Down/Left/Right"),t(" where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+Up/Down/Left/Right"),t(" where the direction is the direction the focus should move to.")],-1),b=e("p",null,[e("img",{src:i,alt:"Multiple panes",title:"Multiple panes"})],-1),g=e("p",null,'Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.',-1),y=e("p",null,[t("To close a pane, you can close all pane items with "),e("kbd",null,"Ctrl+W"),t('. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.')],-1),f=e("p",null,[t("You can split any editor pane horizontally or vertically by using "),e("kbd",null,"Cmd+K"),t(),e("kbd",null,"Up/Down/Left/Right"),t(" where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with "),e("kbd",null,"Cmd+K"),t(),e("kbd",null,"Cmd+Up/Down/Left/Right"),t(" where the direction is the direction the focus should move to.")],-1),v=e("p",null,[e("img",{src:i,alt:"Multiple panes",title:"Multiple panes"})],-1),k=e("p",null,'Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.',-1),C=e("p",null,[t("To close a pane, you can close all pane items with "),e("kbd",null,"Cmd+W"),t('. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.')],-1),Y=e("p",null,[t("You can split any editor pane horizontally or vertically by using "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Up/Down/Left/Right"),t(" where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with "),e("kbd",null,"Ctrl+K"),t(),e("kbd",null,"Ctrl+Up/Down/Left/Right"),t(" where the direction is the direction the focus should move to.")],-1),x=e("p",null,[e("img",{src:i,alt:"Multiple panes",title:"Multiple panes"})],-1),E=e("p",null,'Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.',-1),R=e("p",null,[t("To close a pane, you can close all pane items with "),e("kbd",null,"Ctrl+W"),t('. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.')],-1),L={class:"custom-container tip"},S=e("p",{class:"custom-container-title"},"Tip",-1),T={href:"https://github.com/pulsar-edit/tabs",target:"_blank",rel:"noopener noreferrer"};function D(K,M){const c=h("Tabs"),p=h("ExternalLinkIcon");return u(),d("div",null,[w,l(c,{id:"3",data:[{title:"linux"},{title:"macOS"},{title:"Windows"}],"tab-id":"using-pulsar"},{tab0:n(({title:o,value:a,isActive:s})=>[_,b,g,y]),tab1:n(({title:o,value:a,isActive:s})=>[f,v,k,C]),tab2:n(({title:o,value:a,isActive:s})=>[Y,x,E,R]),_:1}),e("div",L,[S,e("p",null,[t("If you don't like using tabs, you don't have to. You can disable the "),e("a",T,[t("tabs package"),l(p)]),t(" and each pane will still support multiple pane items. You just won't have tabs to use to click between them.")])])])}const O=r(m,[["render",D],["__file","panes.html.vue"]]);export{O as default}; diff --git a/assets/panes.html.7c7f5559.js b/assets/panes.html.7c7f5559.js new file mode 100644 index 0000000000..3e4c5212d4 --- /dev/null +++ b/assets/panes.html.7c7f5559.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-76058cbd","path":"/docs/launch-manual/sections/using-pulsar/sections/panes.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Panes","slug":"panes","link":"#panes","children":[]}],"git":{"updatedTime":1669229414000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":1.71,"words":512},"filePathRelative":"docs/launch-manual/sections/using-pulsar/sections/panes.md"}');export{e as data}; diff --git a/assets/panes.html.8ce248a9.js b/assets/panes.html.8ce248a9.js new file mode 100644 index 0000000000..2d6d020dab --- /dev/null +++ b/assets/panes.html.8ce248a9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0d49e5a8","path":"/docs/atom-archive/using-atom/sections/panes.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Panes","slug":"panes","link":"#panes","children":[]}],"git":{"updatedTime":1664634443000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.84,"words":252},"filePathRelative":"docs/atom-archive/using-atom/sections/panes.md"}');export{e as data}; diff --git a/assets/pending-pane-items.html.0021df08.js b/assets/pending-pane-items.html.0021df08.js new file mode 100644 index 0000000000..e426808e04 --- /dev/null +++ b/assets/pending-pane-items.html.0021df08.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-52fd2aae","path":"/docs/atom-archive/using-atom/sections/pending-pane-items.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Pending Pane Items","slug":"pending-pane-items","link":"#pending-pane-items","children":[]}],"git":{"updatedTime":1664634443000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.76,"words":227},"filePathRelative":"docs/atom-archive/using-atom/sections/pending-pane-items.md"}');export{e as data}; diff --git a/assets/pending-pane-items.html.4cd88c45.js b/assets/pending-pane-items.html.4cd88c45.js new file mode 100644 index 0000000000..fe709bf078 --- /dev/null +++ b/assets/pending-pane-items.html.4cd88c45.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-67da6db9","path":"/docs/launch-manual/sections/using-pulsar/sections/pending-pane-items.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Pending Pane Items","slug":"pending-pane-items","link":"#pending-pane-items","children":[{"level":3,"title":"Disabling Pending Pane Items","slug":"disabling-pending-pane-items","link":"#disabling-pending-pane-items","children":[]}]}],"git":{"updatedTime":1669229414000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":2}]},"readingTime":{"minutes":0.76,"words":227},"filePathRelative":"docs/launch-manual/sections/using-pulsar/sections/pending-pane-items.md"}');export{e as data}; diff --git a/assets/pending-pane-items.html.71de0d56.js b/assets/pending-pane-items.html.71de0d56.js new file mode 100644 index 0000000000..c45f0516e3 --- /dev/null +++ b/assets/pending-pane-items.html.71de0d56.js @@ -0,0 +1 @@ +import{_ as e}from"./allow-pending-pane-items.0fe3dd4a.js";import{_ as i,o as n,c as t,f as l}from"./app.0e1565ce.js";const o={},a=l('"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
',9),s=[a];function c(d,h){return n(),t("div",null,s)}const g=i(o,[["render",c],["__file","pending-pane-items.html.vue"]]);export{g as default}; diff --git a/assets/pending-pane-items.html.b95fbf84.js b/assets/pending-pane-items.html.b95fbf84.js new file mode 100644 index 0000000000..af0ff9d102 --- /dev/null +++ b/assets/pending-pane-items.html.b95fbf84.js @@ -0,0 +1 @@ +import{_ as e}from"./allow-pending-pane-items.0fe3dd4a.js";import{_ as i,o as n,c as t,f as l}from"./app.0e1565ce.js";const o={},a=l('"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
',9),s=[a];function c(d,h){return n(),t("div",null,s)}const g=i(o,[["render",c],["__file","pending-pane-items.html.vue"]]);export{g as default}; diff --git a/assets/photoswipe.esm.09e03fed.js b/assets/photoswipe.esm.09e03fed.js new file mode 100644 index 0000000000..55ad7d5215 --- /dev/null +++ b/assets/photoswipe.esm.09e03fed.js @@ -0,0 +1,4 @@ +/*! + * PhotoSwipe 5.3.3 - https://photoswipe.com + * (c) 2022 Dmytro Semenov + */function m(o,t,e){const i=document.createElement(t||"div");return o&&(i.className=o),e&&e.appendChild(i),i}function u(o,t){return o.x=t.x,o.y=t.y,t.id!==void 0&&(o.id=t.id),o}function M(o){o.x=Math.round(o.x),o.y=Math.round(o.y)}function C(o,t){const e=Math.abs(o.x-t.x),i=Math.abs(o.y-t.y);return Math.sqrt(e*e+i*i)}function I(o,t){return o.x===t.x&&o.y===t.y}function b(o,t,e){return Math.min(Math.max(o,t),e)}function L(o,t,e){let i="translate3d("+o+"px,"+(t||0)+"px,0)";return e!==void 0&&(i+=" scale3d("+e+","+e+",1)"),i}function y(o,t,e,i){o.style.transform=L(t,e,i)}const U="cubic-bezier(.4,0,.22,1)";function R(o,t,e,i){o.style.transition=t?t+" "+e+"ms "+(i||U):"none"}function A(o,t,e){o.style.width=typeof t=="number"?t+"px":t,o.style.height=typeof e=="number"?e+"px":e}function q(o){R(o)}function G(o){return"decode"in o?o.decode().catch(()=>{}):o.complete?Promise.resolve(o):new Promise((t,e)=>{o.onload=()=>t(o),o.onerror=e})}const f={IDLE:"idle",LOADING:"loading",LOADED:"loaded",ERROR:"error"};function X(o){if(o.which===2||o.ctrlKey||o.metaKey||o.altKey||o.shiftKey)return!0}function K(o,t,e=document){let i=[];if(o instanceof Element)i=[o];else if(o instanceof NodeList||Array.isArray(o))i=Array.from(o);else{const s=typeof o=="string"?o:t;s&&(i=Array.from(e.querySelectorAll(s)))}return i}function x(){return!!(navigator.vendor&&navigator.vendor.match(/apple/i))}let F=!1;try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:()=>{F=!0}}))}catch{}class Y{constructor(){this._pool=[]}add(t,e,i,s){this._toggleListener(t,e,i,s)}remove(t,e,i,s){this._toggleListener(t,e,i,s,!0)}removeAll(){this._pool.forEach(t=>{this._toggleListener(t.target,t.type,t.listener,t.passive,!0,!0)}),this._pool=[]}_toggleListener(t,e,i,s,n,r){if(!t)return;const a=n?"removeEventListener":"addEventListener";e.split(" ").forEach(h=>{if(h){r||(n?this._pool=this._pool.filter(p=>p.type!==h||p.listener!==i||p.target!==t):this._pool.push({target:t,type:h,listener:i,passive:s}));const c=F?{passive:s||!1}:!1;t[a](h,i,c)}})}}function B(o,t){if(o.getViewportSizeFn){const e=o.getViewportSizeFn(o,t);if(e)return e}return{x:document.documentElement.clientWidth,y:window.innerHeight}}function S(o,t,e,i,s){let n;if(t.paddingFn)n=t.paddingFn(e,i,s)[o];else if(t.padding)n=t.padding[o];else{const r="padding"+o[0].toUpperCase()+o.slice(1);t[r]&&(n=t[r])}return n||0}function N(o,t,e,i){return{x:t.x-S("left",o,t,e,i)-S("right",o,t,e,i),y:t.y-S("top",o,t,e,i)-S("bottom",o,t,e,i)}}class j{constructor(t){this.slide=t,this.currZoomLevel=1,this.center={},this.max={},this.min={},this.reset()}update(t){this.currZoomLevel=t,this.slide.width?(this._updateAxis("x"),this._updateAxis("y"),this.slide.pswp.dispatch("calcBounds",{slide:this.slide})):this.reset()}_updateAxis(t){const{pswp:e}=this.slide,i=this.slide[t==="x"?"width":"height"]*this.currZoomLevel,n=S(t==="x"?"left":"top",e.options,e.viewportSize,this.slide.data,this.slide.index),r=this.slide.panAreaSize[t];this.center[t]=Math.round((r-i)/2)+n,this.max[t]=i>r?Math.round(r-i)+n:this.center[t],this.min[t]=i>r?n:this.center[t]}reset(){this.center.x=0,this.center.y=0,this.max.x=0,this.max.y=0,this.min.x=0,this.min.y=0}correctPan(t,e){return b(e,this.max[t],this.min[t])}}const z=4e3;class k{constructor(t,e,i,s){this.pswp=s,this.options=t,this.itemData=e,this.index=i}update(t,e,i){this.elementSize={x:t,y:e},this.panAreaSize=i;const s=this.panAreaSize.x/this.elementSize.x,n=this.panAreaSize.y/this.elementSize.y;this.fit=Math.min(1,sNote
Please note that its possible this is outdated, as its original version was published by @'Nathan Sobo' on Jan 3, 2018.
The Provider API allows you to make autocomplete+ awesome. The Pulsar community will ultimately judge the quality of Pulsar's autocomplete experience by the breadth and depth of the provider ecosystem. We're so excited that you're here reading about how to make Pulsar awesome.
The following examples are in CoffeeScript. If you would like to add JavaScript examples, please feel free to edit this page!
provider =
+ # This will work on JavaScript and CoffeeScript files, but not in js comments.
+ selector: '.source.js, .source.coffee'
+ disableForSelector: '.source.js .comment'
+
+ # This will take priority over the default provider, which has an inclusionPriority of 0.
+ # \`excludeLowerPriority\` will suppress any providers with a lower priority
+ # i.e. The default provider will be suppressed
+ inclusionPriority: 1
+ excludeLowerPriority: true
+
+ # This will be suggested before the default provider, which has a suggestionPriority of 1.
+ suggestionPriority: 2
+
+ # Let autocomplete+ filter and sort the suggestions you provide.
+ filterSuggestions: true
+
+ # Required: Return a promise, an array of suggestions, or null.
+ getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix, activatedManually}) ->
+ new Promise (resolve) ->
+ resolve([text: 'something'])
+
+ # (optional): (*experimental*) called when user the user selects a suggestion for the purpose of loading additional information about the suggestion.
+ getSuggestionDetailsOnSelect: (suggestion) ->
+ new Promise (resolve) ->
+ resolve(newSuggestion)
+
+ # (optional): called _after_ the suggestion \`replacementPrefix\` is replaced
+ # by the suggestion \`text\` in the buffer
+ onDidInsertSuggestion: ({editor, triggerPosition, suggestion}) ->
+
+ # (optional): called when your provider needs to be cleaned up. Unsubscribe
+ # from things, kill any processes, etc.
+ dispose: ->
+
The properties of a provider:
`,7),d=o("selector
(required): Defines the scope selector(s) (can be comma-separated) for which your provider should receive suggestion requestsgetSuggestions
(required): Is called when a suggestion request has been dispatched by autocomplete+
to your provider. Return an array of suggestions (if any) in the order you would like them displayed to the user. Returning a Promise of an array of suggestions is also supported.getSuggestionDetailsOnSelect
(optional): (experimental) Is called when a suggestion is selected by the user for the purpose of loading more information about the suggestion. Return a Promise of the new suggestion to replace it with or return null
if no change is needed.disableForSelector
(optional): Defines the scope selector(s) (can be comma-separated) for which your provider should not be usedinclusionPriority
(optional): A number to indicate its priority to be included in a suggestions request. The default provider has an inclusion priority of 0
. Higher priority providers can suppress lower priority providers with excludeLowerPriority
.excludeLowerPriority
(optional): Will not use lower priority providers when this provider is used.suggestionPriority
(optional): A number to determine the sort order of suggestions. The default provider has an suggestion priority of 1
filterSuggestions
(optional): If set to true
, autocomplete+
will perform fuzzy filtering and sorting on the list of matches returned by getSuggestions
.dispose
(optional): Will be called if your provider is being destroyed by autocomplete+
Some providers satisfy a suggestion request in an asynchronous way (e.g. it may need to dispatch requests to an external process to get suggestions). To asynchronously provide suggestions, simply return a Promise
from your getSuggestions
:
getSuggestions: (options) ->
+ return new Promise (resolve) ->
+ # Build your suggestions here asynchronously...
+ resolve(suggestions) # When you are done, call resolve and pass your suggestions to it
+
An options
object will be passed to your getSuggestions
function, with the following properties:
provider =
+ selector: '.source.js, .source.coffee'
+ disableForSelector: '.source.js .comment'
+
+ getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
+ new Promise (resolve) ->
+ # Find your suggestions here
+ suggestion =
+ text: 'someText' # OR
+ snippet: 'someText(\${1:myArg})'
+ displayText: 'someText' # (optional)
+ replacementPrefix: 'so' # (optional)
+ type: 'function' # (optional)
+ leftLabel: '' # (optional)
+ leftLabelHTML: '' # (optional)
+ rightLabel: '' # (optional)
+ rightLabelHTML: '' # (optional)
+ className: '' # (optional)
+ iconHTML: '' # (optional)
+ description: '' # (optional)
+ descriptionMoreURL: '' # (optional)
+ characterMatchIndices: [0, 1, 2] # (optional)
+ resolve([suggestion])
+
Your suggestions should be returned from getSuggestions
as an array of objects with the following properties:
displayText
(optional): A string that will show in the UI for this suggestion. When not set, snippet || text
is displayed. This is useful when snippet
or text
displays too much, and you want to simplify. e.g. {type: 'attribute', snippet: 'class="$0"$1', displayText: 'class'}
replacementPrefix
(optional): The text immediately preceding the cursor, which will be replaced by the text
. If not provided, the prefix passed into getSuggestions
will be used.leftLabelHTML
(optional): Use this instead of leftLabel
if you want to use html for the left label.rightLabel
(optional): An indicator (e.g. function
, variable
) denoting the "kind" of suggestion this representsrightLabelHTML
(optional): Use this instead of rightLabel
if you want to use html for the right label.className
(optional): Class name for the suggestion in the suggestion list. Allows you to style your suggestion via CSS, if desiredautocomplete+
In your package.json
, add:
"providedServices": {
+ "autocomplete.provider": {
+ "versions": {
+ "4.0.0": "provide"
+ }
+ }
+}
+
Then, in your main.coffee
(or whatever file you define as your main
in package.json
i.e. "main": "./lib/your-main"
would imply your-main.coffee
), add the following:
For a single provider:
module.exports =
+ provide: -> @yourProviderHere
+
For multiple providers, just return an array:
module.exports =
+ provide: -> [@yourProviderHere, @yourOtherProviderHere]
+
We've taken to making each provider its own clean repo:
`,11),ts={href:"https://github.com/pulsar-edit/autocomplete-css",target:"_blank",rel:"noopener noreferrer"},is={href:"https://github.com/pular-edit/autocomplete-html",target:"_blank",rel:"noopener noreferrer"},ps={href:"https://github.com/pulsar-edit/autocomplete-snippets",target:"_blank",rel:"noopener noreferrer"},rs=o(`Check out the lib directory in each of these for the code!
The prefix
passed to getSuggestions
may not be sufficient for your language. You may need to generate your own prefix. Here is a pattern to use your own prefix:
provider =
+ selector: '.source.js, .source.coffee'
+
+ getSuggestions: ({editor, bufferPosition}) ->
+ prefix = @getPrefix(editor, bufferPosition)
+
+ new Promise (resolve) ->
+ suggestion =
+ text: 'someText'
+ replacementPrefix: prefix
+ resolve([suggestion])
+
+ getPrefix: (editor, bufferPosition) ->
+ # Whatever your prefix regex might be
+ regex = /[\\w0-9_-]+$/
+
+ # Get the text for the line up to the triggered buffer position
+ line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition])
+
+ # Match the regex to the line, and return the match
+ line.match(regex)?[0] or ''
+
package.json
file has name
, description
, and repository
fields.package.json
name
is URL Safe, as in it's not an emoji or special character.package.json
file has a version
field with a value of "0.0.0"
.package.json
file has an engines
field that contains an entry for atom
such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ pulsar -p publish minor
+
Your package is now published and available on Pulsar Package Repository. Head on over to https://web.pulsar-edit.dev/packages/your-package-name
to see your package's page.
With pulsar -p publish
, you can bump the version and publish by using
$ pulsar -p publish <version-type>
+
where version-type
can be major
, minor
and patch
.
i.e. to bump a package from v1.0.0 to v1.1.0:
$ pulsar -p publish minor
+
package.json
file has name
, description
, and repository
fields.package.json
file has a version
field with a value of "0.0.0"
.package.json
file has an engines
field that contains an entry for Atom such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ apm publish minor
+
Your package is now published and available on atom.io. Head on over to https://atom.io/packages/your-package-name
to see your package's page.
With apm publish
, you can bump the version and publish by using
$ apm publish <em>version-type</em>
+
where version-type
can be major
, minor
and patch
.
The major
option to the publish command tells apm to increment the first number of the version before publishing so the published version will be 1.0.0
and the Git tag created will be v1.0.0
.
The minor
option to the publish command tells apm to increment the second number of the version before publishing so the published version will be 0.1.0
and the Git tag created will be v0.1.0
.
The patch
option to the publish command tells apm to increment the third number of the version before publishing so the published version will be 0.0.1
and the Git tag created will be v0.0.1
.
Now that Pulsar is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Pulsar for the first time, you should get a screen that looks like this:
This is the Pulsar welcome screen and gives you a pretty good starting point for how to get started with the editor.
In that welcome screen, we are introduced to probably the most important command in Pulsar, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like LNX: Ctrl+Shift+P - MAC: Cmd+Shift+P - WIN: Ctrl+Shift+P to demonstrate how to run a command and tabbed sections where necessary where instructions for different platforms may differ.
If you have customized your Pulsar keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Pulsar. Instead of clicking around all the application menus to look for something, you can press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut key strokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Pulsar has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Pulsar Packages.
To open the Settings View, you can:
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Pulsar. Pulsar ships with 4 different UI themes, dark and light variants of the Pulsar and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
',17),T={href:"https://web.pulsar-edit.dev",target:"_blank",rel:"noopener noreferrer"},F=e("h4",{id:"soft-wrap",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#soft-wrap","aria-hidden":"true"},"#"),t(" Soft Wrap")],-1),x=e("p",null,"You can use the Settings View to specify your whitespace and wrapping preferences.",-1),z=e("p",null,[e("img",{src:p,alt:"Whitespace and wrapping preferences settings"})],-1),A=e("p",null,[t('Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the '),e("kbd",null,"Tab"),t(' key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.')],-1),I=e("p",null,'The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.',-1),O=l('Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Pulsar. You can do it by choosing File > Open from the menu bar or by pressing
LNX/WIN: Ctrl+O - MAC: Cmd+O
to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
Another way to open a file in Pulsar is from the command line using the pulsar
command.
You can run the pulsar
command with one or more file paths to open up those files in Pulsar.
$ pulsar --help
+> Pulsar Editor v1.100.0
+
+> Usage: pulsar [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Pulsar window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off pulsar [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+getting-started/sections/pulsar-basics.md:130:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ pulsar getting-started/sections/pulsar-basics.md:130
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+getting-started/sections/pulsar-basics.md.md:150:722
+$ pulsar getting-started/sections/pulsar-basics.md:150:722
+
You can open any number of directories from the command line by passing their paths to the pulsar
command line tool. For example, you could run the command pulsar ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Pulsar with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
',4),D=e("p",null,[t("You can also hide and show it with "),e("kbd",null,"Ctrl+\\"),t(" or the "),e("code",null,"tree-view: toggle"),t(" command from the Command Palette, and "),e("kbd",null,"Alt+\\"),t(" will focus it. When the Tree view has focus you can press "),e("kbd",null,"A"),t(", "),e("kbd",null,"M"),t(", or "),e("kbd",null,"Delete"),t(" to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in your native filesystem or copying the file path to the clipboard.")],-1),J=e("p",null,[t("You can also hide and show it with "),e("kbd",null,"Cmd+\\"),t(" or the "),e("code",null,"tree-view: toggle"),t(" command from the Command Palette, and "),e("kbd",null,"Ctrl+0"),t(" will focus it. When the Tree view has focus you can press "),e("kbd",null,"A"),t(", "),e("kbd",null,"M"),t(", or "),e("kbd",null,"Delete"),t(" to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in Finder or copying the file path to the clipboard.")],-1),H=e("p",null,[t("You can also hide and show it with "),e("kbd",null,"Ctrl+\\"),t(" or the "),e("code",null,"tree-view: toggle"),t(" command from the Command Palette, and "),e("kbd",null,"Alt+\\"),t(" will focus it. When the Tree view has focus you can press "),e("kbd",null,"A"),t(", "),e("kbd",null,"M"),t(", or "),e("kbd",null,"Delete"),t(" to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in Windows Explorer or copying the file path to the clipboard.")],-1),K=e("div",{class:"custom-container note"},[e("p",{class:"custom-container-title"},"Note"),e("p",null,[e("strong",null,"Pulsar Packages")]),e("p",null,"Like many parts of Pulsar, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Pulsar by default. Packages that are bundled with Pulsar are referred to as Core packages. Ones that aren't bundled with Pulsar are referred to as Community packages."),e("p",null,"You can find the source code to the Tree View on GitHub at https://github.com/pulsar-edit/tree-view."),e("p",null,"This is one of the interesting things about Pulsar. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.")],-1),Q=e("h4",{id:"opening-a-file-in-a-project",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#opening-a-file-in-a-project","aria-hidden":"true"},"#"),t(" Opening a File in a Project")],-1),Z=e("p",null,"Once you have a project open in Pulsar, you can easily find and open any file within that project.",-1),ee=e("p",null,[t("If you press "),e("kbd",null,"Ctrl+T"),t(" or "),e("kbd",null,"Ctrl+P"),t(", the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.")],-1),te=e("p",null,[e("img",{src:u,alt:"Opening files with the Fuzzy Finder",title:"Opening files with the Fuzzy Finder"})],-1),ne=e("p",null,[t("You can also search through only the files currently opened (rather than every file in your project) with "),e("kbd",null,"Ctrl+B"),t('. This searches through your "buffers" or open files. You can also limit this fuzzy search with '),e("kbd",null,"Ctrl+Shift+B"),t(", which searches only through the files which are new or have been modified since your last Git commit.")],-1),oe=e("p",null,[t("If you press "),e("kbd",null,"Cmd+T"),t(" or "),e("kbd",null,"Cmd+P"),t(", the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.")],-1),ie=e("p",null,[e("img",{src:u,alt:"Opening files with the Fuzzy Finder",title:"Opening files with the Fuzzy Finder"})],-1),se=e("p",null,[t("You can also search through only the files currently opened (rather than every file in your project) with "),e("kbd",null,"Cmd+B"),t('. This searches through your "buffers" or open files. You can also limit this fuzzy search with '),e("kbd",null,"Cmd+Shift+B"),t(", which searches only through the files which are new or have been modified since your last Git commit.")],-1),ae=e("p",null,[t("If you press "),e("kbd",null,"Ctrl+T"),t(" or "),e("kbd",null,"Ctrl+P"),t(", the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.")],-1),le=e("p",null,[e("img",{src:u,alt:"Opening files with the Fuzzy Finder",title:"Opening files with the Fuzzy Finder"})],-1),re=e("p",null,[t("You can also search through only the files currently opened (rather than every file in your project) with "),e("kbd",null,"Ctrl+B"),t('. This searches through your "buffers" or open files. You can also limit this fuzzy search with '),e("kbd",null,"Ctrl+Shift+B"),t(", which searches only through the files which are new or have been modified since your last Git commit.")],-1),de=e("code",null,"core.ignoredNames",-1),he=e("code",null,"fuzzy-finder.ignoredNames",-1),ce=e("code",null,"core.excludeVCSIgnoredPaths",-1),ue={href:"https://git-scm.com/docs/gitignore",target:"_blank",rel:"noopener noreferrer"},pe=e("code",null,".gitignore",-1),me=e("code",null,"core.ignoredNames",-1),fe=e("code",null,"fuzzy-finder.ignoredNames",-1),ge={href:"https://github.com/isaacs/minimatch",target:"_blank",rel:"noopener noreferrer"},ye=l('Tip
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Pulsar, it will show up in the Settings View under the "Packages" tab, along with all the pre-installed packages that come with Pulsar. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Pulsar will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Pulsar from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on pulsar-edit.dev, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
Clicking on "Install" will install the theme and make it available in the Theme dropdowns as we saw in Changing the Theme.
You can also install packages or themes from the command line using ppm (Pulsar Package Manager). This is used by running pulsar -p <commmand>
or pulsar --package <command>
.
Tip
Check that you have ppm available by running the following command in your terminal:
$ pulsar -p help install
+
You should see a message print out with details about the pulsar -p install
command.
If you do not, see the Installing Pulsar section for instructions on how to install the pulsar
command for your system.
You can install packages by using the pulsar -p install
command:
pulsar -p install <package_name>
to install the latest version.pulsar -p install <package_name>@<package_version>
to install a specific version.You can also use ppm to find new packages to install. If you run pulsar -p search
, you can search the package registry for a search term.
$ pulsar -p search linter
+> Search Results For 'linter' (30)
+> \u251C\u2500\u2500 linter A Base Linter with Cow Powers (9863242 downloads, 4757 stars)
+> \u251C\u2500\u2500 linter-ui-default Default UI for the Linter package (7755748 downloads, 1201 stars)
+> \u251C\u2500\u2500 linter-eslint Lint JavaScript on the fly, using ESLint (v7 or older) (2418043 downloads, 1660 stars)
+> \u251C\u2500\u2500 linter-jshint Linter plugin for JavaScript, using jshint (1202044 downloads, 1271 stars)
+> \u251C\u2500\u2500 linter-gcc Lint C and C++ source files using gcc / g++ (863989 downloads, 194 stars)
+> ...
+> \u251C\u2500\u2500 linter-shellcheck Lint Bash on the fly, using shellcheck (136938 downloads, 280 stars)
+> \u2514\u2500\u2500 linter-rust Lint Rust-files, using rustc and/or cargo (132550 downloads, 91 stars)
+
You can use pulsar -p view
to see more information about a specific package.
$ pulsar -p view linter
+> linter
+> \u251C\u2500\u2500 3.4.0
+> \u251C\u2500\u2500 https://github.com/steelbrain/linter
+> \u251C\u2500\u2500 A Base Linter with Cow Powers
+> \u251C\u2500\u2500 9863242 downloads
+> \u2514\u2500\u2500 4757 stars
+>
+> Run \`pulsar -p install linter\` to install this package.
+
Pulsar can install from a GitHub repository or any valid git remote url. The -b
option can then be used to specify a particular tag or branch.
Git remotepulsar -p install <git_remote> [-b <branch_or_tag>]
GitHubpulsar -p install <github_username>/<github_project> [-b <branch_or_tag>]
Note
Please note that its possible this is outdated, as its original version was published by @'David Wilson' on Jan 22, 2018.
Here is a quick summary to see all the changes at a glance:
Before | +/- | After |
---|---|---|
atom-text-editor::shadow {} | - ::shadow | atom-text-editor {} |
.class /deep/ .class {} | - /deep/ | .class .class {} |
atom-text-editor, :host {} | - :host | atom-text-editor {} |
.comment {} | + .syntax-- | .syntax--comment {} |
Below you'll find more detailed examples.
::shadow
Remove the ::shadow
pseudo-element selector from atom-text-editor
.
Before:
atom-text-editor::shadow .cursor {
+ border-color: hotpink;
+}
+
After:
atom-text-editor .cursor {
+ border-color: hotpink;
+}
+
/deep/
Remove the /deep/
combinator selector. It didn't get used that often, mainly to customize the scrollbars.
Before:
.scrollbars-visible-always /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
After:
.scrollbars-visible-always ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
:host
Remove the :host
pseudo-element selector. To scope any styles to the text editor, using atom-text-editor
is already enough.
Before:
atom-text-editor,
+:host {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
After:
atom-text-editor {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
syntax--
Add a syntax--
prefix to all grammar selectors used by language packages. It looks a bit verbose, but it will protect all the grammar selectors from accidental style pollution.
Before:
.comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .markup.link {
+ color: @mono-3;
+ }
+}
+
After:
.syntax--comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .syntax--markup.syntax--link {
+ color: @mono-3;
+ }
+}
+
Note: Selectors like the .gutter
, .indent-guide
, .cursor
among others, that are also part of Syntax themes, don't need a prefix. Only grammar selectors that get used by language packages. For example .syntax--keyword
, .syntax--keyword.syntax--operator.syntax--js
.
Atom also allowed you to target a specific shadow DOM context with an entire style sheet by adding .atom-text-editor
to the file name. This is now not necessary anymore and can be removed.
Before:
my-ui-theme/
+ styles/
+ index.atom-text-editor.less
+
After:
my-ui-theme/
+ styles/
+ index.less
+
By replacing atom-text-editor::shadow
with atom-text-editor.editor
, specificity might have changed. This can cause the side effect that some of your properties won't be strong enough. To fix that, you can increase specificity on your selector. A simple way is to just repeat your class (in the example below it's .my-class
):
Before:
atom-text-editor::shadow .my-class {
+ color: hotpink;
+}
+
After:
atom-text-editor .my-class.my-class {
+ color: hotpink;
+}
+
Pulsar supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
`,2),w={href:"https://atom.io/docs/api/latest/TextEditor#instance-getRootScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},T=s("code",null,"Editor::getRootScopeDescriptor",-1),P=s("code",null,'[".source.js"]',-1),A={href:"https://atom.io/docs/api/latest/TextEditor#instance-scopeDescriptorForBufferPosition",target:"_blank",rel:"noopener noreferrer"},O=s("code",null,"Editor::scopeDescriptorForBufferPosition",-1),C={href:"https://atom.io/docs/api/latest/Cursor#instance-getScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},I=s("code",null,"Cursor::getScopeDescriptor",-1),E=s("code",null,'["source.js", "meta.function.js", "entity.name.function.js"]',-1),J=o(`Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
Atom supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
`,2),w={href:"https://atom.io/docs/api/latest/TextEditor#instance-getRootScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},D=n("code",null,"Editor::getRootScopeDescriptor",-1),C=n("code",null,'[".source.js"]',-1),A={href:"https://atom.io/docs/api/latest/TextEditor#instance-scopeDescriptorForBufferPosition",target:"_blank",rel:"noopener noreferrer"},E=n("code",null,"Editor::scopeDescriptorForBufferPosition",-1),T={href:"https://atom.io/docs/api/latest/Cursor#instance-getScopeDescriptor",target:"_blank",rel:"noopener noreferrer"},J=n("code",null,"Cursor::getScopeDescriptor",-1),L=n("code",null,'["source.js", "meta.function.js", "entity.name.function.js"]',-1),B=t(`Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer\u2014the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Atom to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Atom will only attempt to call deserialize if the two versions match, and otherwise return undefined. We plan on implementing a migration system in the future, but this at least protects you from improperly deserializing old state.
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer\u2014the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Pulsar to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Pulsar will only attempt to call deserialize if the two versions match, and otherwise return undefined.
Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Atom, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your ~/.atom
%USERPROFILE%\\.atom
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Atom. You can also easily open up that file by selecting the Atom > SnippetsEdit > SnippetsFile > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (\${1:true}) {
+ $2
+ } else if (\${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
\u{1F4A5} just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Atom should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Pulsar, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\\.pulsar
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Pulsar. You can also easily open up that file by selecting the LNX: Edit > Snippets - MAC: Pulsar > Snippets - WIN: File > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(\${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
Warning
Snippet keys, unlike CSS selectors, can only be repeated once per level. If there are duplicate keys at the same level, then only the last one will be read. See Configuring with CSON for more information.
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (\${1:true}) {
+ $2
+ } else if (\${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
\u{1F4A5} Just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Pulsar should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
Again, see Configuring with CSON for more information on CSON key structure and non-repeatability.
Note
Please note that its possible this is outdated, as its original version was published by @'Phil' on Jan 24, 2017.
The builtin SymbolProvider
allows showing the types of symbols in the suggestion list.
The icon colors are intended to match the syntax color of the symbol type. e.g. functions are blue in this theme, so the function icon is also blue.
',5),b=e("code",null,"SymbolProvider",-1),h={href:"https://github.com/pulsar-edit/language-coffee-script/blob/d86c8963dcee0ab811da05a175b2218045d0c124/settings/language-coffee-script.cson#L5",target:"_blank",rel:"noopener noreferrer"},y=a(`# An example for the CoffeeScript language
+'.source.coffee':
+ autocomplete:
+ symbols:
+ class:
+ selector: '.class.name, .inherited-class, .instance.type'
+ typePriority: 4
+ function:
+ selector: '.function.name'
+ typePriority: 3
+ variable:
+ selector: '.variable'
+ typePriority: 2
+ '': # the catch-all
+ selector: '.source'
+ typePriority: 1
+ builtin:
+ suggestions: [
+ 'one'
+ 'two'
+ ]
+
A more generic example:
+'.source.language':
+ autocomplete:
+ symbols:
+ typename1:
+ selector: '.some, .selectors'
+ typePriority: 1
+ typename2:
+ suggestions: [...]
+ typename3:
+ ...
+
Any number of Typename objects are supported, and typename
can be anything you choose. However, you are encouraged to use one of the predefined typenames
. There are predefined styles for the following types:
builtin
+class
+constant
+function
+import
+keyword
+method
+module
+mixin
+package
+property
+require
+snippet
+tag
+type
+value
+variable
+
Typename objects support the following properties:
`,7),g=a("selector
: The selector that matches your symbol types. e.g. '.variable.name'
. You can also have several selectors separated by commas, just like in CSS '.variable.name, .storage.variable'
typePriority
: The priority this Typename object has over others. e.g. in our CoffeeScript example above, if a symbol is tagged with the function
type in one part of the code, but class
in another part of the code, it will be displayed to the user as a class
because class
has a higher typePriority
You can open the init.coffee
file in an editor from the Atom > Init ScriptFile > Init ScriptEdit > Init Script menu. This file can also be named init.js
and contain JavaScript code.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.coffee
file to have Atom greet you with an audio beep every time it loads:
atom.beep()
+
atom.commands.add 'atom-text-editor', 'markdown:paste-as-link', ->
+ return unless editor = atom.workspace.getActiveTextEditor()
+
+ selection = editor.getLastSelection()
+ clipboardText = atom.clipboard.read()
+
+ selection.insertText("[#{selection.getText()}](#{clipboardText})")
+
You can open the init.js
file in an editor from the LNX: Atom > Init Script - MAC: File > Init Script - WIN: Edit > Init Script menu.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.js
file to have Pulsar greet you with an audio beep every time it loads:
atom.beep();
+
atom.commands.add("atom-text-editor", "markdown:paste-as-link", () => {
+ let clipboardText, editor, selection;
+ if (!(editor = atom.workspace.getActiveTextEditor())) {
+ return;
+ }
+ selection = editor.getLastSelection();
+ clipboardText = atom.clipboard.read();
+ return selection.insertText(
+ "[" + selection.getText() + "](" + clipboardText + ")"
+ );
+});
+
Now, reload Pulsar and use the Command Palette to execute the new command, Markdown: Paste As Link
, by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a keybinding for the command.
If you're running Windows or Linux and you don't see the menu bar, it may have been accidentally toggled it off. You can bring it back from the Command Palette with Window: Toggle Menu Bar
or by pressing Alt.
You can disable hiding the menu bar with Alt by unchecking Settings > Core > Auto Hide Menu Bar
.
MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
To begin, there are a few things we'll assume you know, at least to some degree. Since all of Atom is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.
While much of Atom has been converted to JavaScript, a lot of older code has been left implemented in CoffeeScript because changing it would have been too risky. Additionally, Atom's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:
MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
This document will guide you through the large bits of upgrading your package to work with 1.0 APIs.
We've set deprecation messages and errors in strategic places to help make sure you don't miss anything. You should be able to get 95% of the way to an updated package just by fixing errors and deprecations. There are a couple of things you can do to get the full effect of all the errors and deprecations.
If you use any class from require 'atom'
with a $
or View
in the name, add the atom-space-pen-views
module to your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
Then run apm install
in your package directory.
Anywhere you are requiring one of the following from atom
you need to require them from atom-space-pen-views
instead.
# require these from 'atom-space-pen-views' rather than 'atom'
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
So this:
# Old way
+{$, TextEditorView, View, GitRepository} = require 'atom'
+
Would be replaced by this:
# New way
+{GitRepository} = require 'atom'
+{$, TextEditorView, View} = require 'atom-space-pen-views'
+
You wrote specs, right!? Here's where they shine. Run them with cmd-shift-P
, and search for run package specs
. It will show all the deprecation messages and errors.
When you are deprecation free and all done converting, upgrade the engines
field in your package.json:
{
+ "engines": {
+ "atom": ">=0.174.0 <2.0.0"
+ }
+}
+
All of the methods in Atom core that have changes will emit deprecation messages when called. These messages are shown in two places: your package specs, and in Deprecation Cop.
Just run your specs, and all the deprecations will be displayed in yellow.
Note
Note: Deprecations are only displayed when executing specs through the "Window: Run Package Specs" command in the Atom UI. Deprecations are not displayed when running specs at the terminal.
Run Atom in Dev Mode, atom --dev
, with your package loaded, and open Deprecation Cop (search for "deprecation" in the command palette). Deprecated methods will appear in Deprecation Cop only after they have been called.
When Deprecation Cop is open, and deprecated methods are called, a Refresh
button will appear in the top right of the Deprecation Cop interface. So exercise your package, then come back to Deprecation Cop and click the Refresh
button.
Previous to 1.0, views were baked into Atom core. These views were based on jQuery and space-pen
. They looked something like this:
# The old way: getting views from atom
+{$, TextEditorView, View} = require 'atom'
+
+module.exports =
+class SomeView extends View
+ @content: ->
+ @div class: 'find-and-replace', =>
+ @div class: 'block', =>
+ @subview 'myEditor', new TextEditorView(mini: true)
+ #...
+
require 'atom'
no longer provides view helpers or jQuery. Atom Core is now 'view agnostic'. The preexisting view system is available from a new Node module: atom-space-pen-views.
atom-space-pen-views now provides jQuery, space-pen views, and Atom specific views:
# These are now provided by atom-space-pen-views
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
To use the new views, you need to specify the atom-space-pen-views module in your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
space-pen bundles jQuery. If you do not need space-pen or any of the views, you can require jQuery directly.
{
+ "dependencies": {
+ "jquery": "^2"
+ }
+}
+
Sometimes it is as simple as converting the requires at the top of each view page. I assume you read the 'TL;DR' section and have updated all of your requires.
afterAttach
and beforeRemove
updatedThe afterAttach
and beforeRemove
hooks have been replaced with attached
and detached
and the semantics have changed.
afterAttach
was called whenever the node was attached to another DOM node, even if that parent node wasn't present in the DOM. afterAttach
also was called with a boolean indicating whether or not the element and its parents were on the DOM. Now the attached
hook is only called when the node and all of its parents are actually on the DOM, and is not called with a boolean.
beforeRemove
was only called when $.fn.remove
was called, which was typically used when the node was completely removed from the DOM. The new detached
hook is called whenever the DOM node is detached, which could happen if the node is being detached for reattachment later. In short, if beforeRemove
is called the node is never coming back. With detached
it might be attached again later.
# Old way
+{View} = require 'atom'
+class MyView extends View
+ afterAttach: (onDom) ->
+ #...
+
+ beforeRemove: ->
+ #...
+
# New way
+{View} = require 'atom-space-pen-views'
+class MyView extends View
+ attached: ->
+ # Always called with the equivalent of @afterAttach(true)!
+ #...
+
+ detached: ->
+ #...
+
subscribe
and subscribeToCommand
methods removedThe subscribe
and subscribeToCommand
methods have been removed. See the Eventing and Disposables section for more info.
The ScrollView
has very minor changes.
You can no longer use @off
to remove default behavior for core:move-up
, core:move-down
, etc.
# Old way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ super()
+ # turn off default scrolling behavior from ScrollView
+ @off 'core:move-up'
+ @off 'core:move-down'
+ @off 'core:move-left'
+ @off 'core:move-right'
+
# New way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ disposable = super()
+ # turn off default scrolling behavior from ScrollView
+ disposable.dispose()
+
Your SelectListView
might look something like this:
# Old!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ @addClass('command-palette overlay from-top')
+ atom.workspaceView.command 'command-palette:toggle', => @toggle()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ if @hasParent()
+ @cancel()
+ else
+ @attach()
+
+ attach: ->
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ atom.workspaceView.append(this)
+ @focusFilterEditor()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+
This attaches and detaches itself from the DOM when toggled, canceling magically detaches it from the DOM, and it uses the classes overlay
and from-top
.
The new SelectListView no longer automatically detaches itself from the DOM when cancelled. It's up to you to implement whatever cancel behavior you want. Using the new APIs to mimic the semantics of the old class, it should look like this:
# New!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ # no more need for the \`overlay\` and \`from-top\` classes
+ @addClass('command-palette')
+ atom.commands.add 'atom-workspace', 'command-palette:toggle', => @toggle()
+
+ # You need to implement the \`cancelled\` method and hide.
+ cancelled: ->
+ @hide()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ # Toggling now checks panel visibility,
+ # and hides / shows rather than attaching to / detaching from the DOM.
+ if @panel?.isVisible()
+ @cancel()
+ else
+ @show()
+
+ show: ->
+ # Now you will add your select list as a modal panel to the workspace
+ @panel ?= atom.workspace.addModalPanel(item: this)
+ @panel.show()
+
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ @focusFilterEditor()
+
+ hide: ->
+ @panel?.hide()
+
The API no longer exposes any specialized view objects or view classes. atom.workspaceView
, and all the view classes: WorkspaceView
, EditorView
, PaneView
, etc. have been globally deprecated.
Nearly all of the atom-specific actions performed by the old view objects can now be managed via the model layer. For example, here's adding a panel to the interface using the atom.workspace
model instead of the workspaceView
:
# Old!
+div = document.createElement('div')
+atom.workspaceView.appendToTop(div)
+
# New!
+div = document.createElement('div')
+atom.workspace.addTopPanel(item: div)
+
For actions that still require the view, such as dispatching commands or munging css classes, you'll access the view via the atom.views.getView()
method. This will return a subclass of HTMLElement
rather than a jQuery object or an instance of a deprecated view class (e.g. WorkspaceView
).
# Old!
+workspaceView = atom.workspaceView
+editorView = workspaceView.getActiveEditorView()
+paneView = editorView.getPaneView()
+
# New!
+# Generally, just use the models
+workspace = atom.workspace
+editor = workspace.getActiveTextEditor()
+pane = editor.getPane()
+
+# If you need views, get them with \`getView\`
+workspaceElement = atom.views.getView(atom.workspace)
+editorElement = atom.views.getView(editor)
+paneElement = atom.views.getView(pane)
+
atom.workspaceView
, the WorkspaceView
class and the EditorView
class have been deprecated. These two objects are used heavily throughout specs, mostly to dispatch events and commands. This section will explain how to remove them while still retaining the ability to dispatch events and commands.
WorkspaceView
referencesWorkspaceView
has been deprecated. Everything you could do on the view, you can now do on the Workspace
model.
Requiring WorkspaceView
from atom
and accessing any methods on it will throw a deprecation warning. Many specs lean heavily on WorkspaceView
to trigger commands and fetch EditorView
objects.
Your specs might contain something like this:
# Old!
+{WorkspaceView} = require 'atom'
+describe 'FindView', ->
+ beforeEach ->
+ atom.workspaceView = new WorkspaceView()
+
Instead, we will use the atom.views.getView()
method. This will return a plain HTMLElement
, not a WorkspaceView
or jQuery object.
# New!
+describe 'FindView', ->
+ workspaceElement = null
+ beforeEach ->
+ workspaceElement = atom.views.getView(atom.workspace)
+
The workspace needs to be attached to the DOM in some cases. For example, view hooks only work (attached()
on View
, attachedCallback()
on custom elements) when there is a descendant attached to the DOM.
You might see this in your specs:
# Old!
+atom.workspaceView.attachToDom()
+
Change it to:
# New!
+jasmine.attachToDOM(workspaceElement)
+
Like WorkspaceView
, EditorView
has been deprecated. Everything you needed to do on the view you are now able to do on the TextEditor
model.
In many cases, you will not even need to get the editor's view anymore. Any of those instances should be updated to use the TextEditor
instance instead. You should really only need the editor's view when you plan on triggering a command on the view in a spec.
Your specs might contain something like this:
# Old!
+describe 'Something', ->
+ [editorView] = []
+ beforeEach ->
+ editorView = atom.workspaceView.getActiveView()
+
We're going to use atom.views.getView()
again to get the editor element. As in the case of the workspaceElement
, getView
will return a subclass of HTMLElement
rather than an EditorView
or jQuery object.
# New!
+describe 'Something', ->
+ [editor, editorElement] = []
+ beforeEach ->
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
Since the editorElement
objects are no longer jQuery objects, they no longer support trigger()
. Additionally, Atom has a new command dispatcher, atom.commands
, that we use rather than commandeering jQuery's trigger
method.
From this:
# Old!
+workspaceView.trigger 'a-package:toggle'
+editorView.trigger 'find-and-replace:show'
+
To this:
# New!
+atom.commands.dispatch workspaceElement, 'a-package:toggle'
+atom.commands.dispatch editorElement, 'find-and-replace:show'
+
A couple large things changed with respect to events:
`,38),S={href:"https://atom.io/docs/api/latest/Disposable",target:"_blank",rel:"noopener noreferrer"},N=n("code",null,"Disposable",-1),M=n("li",null,[s("The "),n("code",null,"subscribe()"),s(" method is no longer available on "),n("code",null,"space-pen"),s(),n("code",null,"View"),s(" objects")],-1),R=n("li",null,[s("An Emitter is now provided from "),n("code",null,"require 'atom'")],-1),P=n("h5",{id:"consuming-events",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#consuming-events","aria-hidden":"true"},"#"),s(" Consuming Events")],-1),W={href:"https://atom.io/docs/api/latest/Disposable",target:"_blank",rel:"noopener noreferrer"},I=n("code",null,"Disposable",-1),$=n("code",null,"dispose()",-1),L=o(`# Old!
+editor.on 'changed', ->
+
# New!
+disposable = editor.onDidChange ->
+
+# You can unsubscribe at some point in the future via \`dispose()\`
+disposable.dispose()
+
Deprecation warnings will guide you toward the correct methods.
CompositeDisposable
You can group multiple disposables into a single disposable with a CompositeDisposable
.
{CompositeDisposable} = require 'atom'
+
+class Something
+ constructor: ->
+ editor = atom.workspace.getActiveTextEditor()
+ @disposables = new CompositeDisposable
+ @disposables.add editor.onDidChange ->
+ @disposables.add editor.onDidChangePath ->
+
+ destroy: ->
+ @disposables.dispose()
+
View::subscribe
and Subscriber::subscribe
callsThere were a couple permutations of subscribe()
. In these examples, a CompositeDisposable
is used as it will commonly be useful where conversion is necessary.
subscribe(unsubscribable)
This one is very straight forward.
# Old!
+@subscribe editor.on 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(modelObject, event, method)
When the modelObject is an Atom model object, the change is very simple. Just use the correct event method, and add it to your CompositeDisposable.
# Old!
+@subscribe editor, 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(jQueryObject, selector(optional), event, method)
Things are a little more complicated when subscribing to a DOM or jQuery element. Atom no longer provides helpers for subscribing to elements. You can use jQuery or the native DOM APIs, whichever you prefer.
# Old!
+@subscribe $(window), 'focus', ->
+
# New!
+{Disposable, CompositeDisposable} = require 'atom'
+disposables = new CompositeDisposable
+
+# New with jQuery
+focusCallback = ->
+$(window).on 'focus', focusCallback
+disposables.add new Disposable ->
+ $(window).off 'focus', focusCallback
+
+# New with native APIs
+focusCallback = ->
+window.addEventListener 'focus', focusCallback
+disposables.add new Disposable ->
+ window.removeEventListener 'focus', focusCallback
+
Emitter
# New!
+{Emitter} = require 'atom'
+
+class Something
+ constructor: ->
+ @emitter = new Emitter
+
+ destroy: ->
+ @emitter.dispose()
+
+ onDidChange: (callback) ->
+ @emitter.on 'did-change', callback
+
+ methodThatFiresAChange: ->
+ @emitter.emit 'did-change', {data: 2}
+
+# Using the evented class
+something = new Something
+something.onDidChange (eventObject) ->
+ console.log eventObject.data # => 2
+something.methodThatFiresAChange()
+
# Old!
+atom.workspaceView.command 'core:close core:cancel', ->
+
+# When inside a View class, you might see this
+@subscribeToCommand 'core:close core:cancel', ->
+
# New!
+@disposables.add atom.commands.add 'atom-workspace',
+ 'core:close': ->
+ 'core:cancel': ->
+
+# You can register commands directly on individual DOM elements in addition to
+# using selectors. When in a View class, you should have a \`@element\` object
+# available. \`@element\` is a plain HTMLElement object
+@disposables.add atom.commands.add @element,
+ 'core:close': ->
+ 'core:cancel': ->
+
Here's an example from Atom's light syntax theme. Note that the atom-text-editor
selector intended to target the editor from the outside has been retained to allow the theme to keep working during the transition phase when it is possible to disable the shadow DOM.
atom-text-editor,
+:host {
+ /* :host added */
+ background-color: @syntax-background-color;
+ color: @syntax-text-color;
+
+ .invisible-character {
+ color: @syntax-invisible-character-color;
+ }
+ /* more nested selectors... */
+}
+
Note
Note: The Shadow DOM was removed in Atom 1.13
. The ::shadow
and /deep/
selectors and the context-targeted style sheets described below won't work and should not be used anymore.
In addition to changes in Atom's scripting API, we'll also be making some breaking changes to Atom's DOM structure, requiring style sheets and keymaps in both packages and themes to be updated.
Deprecation Cop will list usages of deprecated selector patterns to guide you. You can access it via the Command Palette (cmd-shift-p
, then search for Deprecation
). It breaks the deprecations down by package:
Rather than adding classes to standard HTML elements to indicate their role, Atom now uses custom element names. For example, <div class="workspace">
has now been replaced with <atom-workspace>
. Selectors should be updated accordingly. Note that tag names have lower specificity than classes in CSS, so you'll need to take care in converting things.
Old Selector | New Selector |
---|---|
.editor | atom-text-editor |
.editor.mini | atom-text-editor[mini] |
.workspace | atom-workspace |
.horizontal | atom-workspace-axis.horizontal |
.vertical | atom-workspace-axis.vertical |
.pane-container | atom-pane-container |
.pane | atom-pane |
.tool-panel | atom-panel |
.panel-top | atom-panel.top |
.panel-bottom | atom-panel.bottom |
.panel-left | atom-panel.left |
.panel-right | atom-panel.right |
.overlay | atom-panel.modal |
::shadow
The ::shadow
pseudo-element allows you to bypass a single shadow root. For example, say you want to update a highlight decoration for a linter package. Initially, the style looks as follows:
// Without shadow DOM support
+atom-text-editor .highlight.my-linter {
+ background: hotpink;
+}
+
In order for this style to apply with the shadow DOM enabled, you will need to add a second selector with the ::shadow
pseudo-element. You should leave the original selector in place so your theme continues to work with the shadow DOM disabled during the transition period.
// With shadow DOM support
+atom-text-editor .highlight.my-linter,
+atom-text-editor::shadow .highlight.my-linter {
+ background: hotpink;
+}
+
/deep/
The /deep/
combinator overrides all shadow boundaries, making it useful for rules you want to apply globally such as scrollbar styling. Here's a snippet containing scrollbar styling for the Atom Dark UI theme before shadow DOM support:
// Without shadow DOM support
+.scrollbars-visible-always {
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track,
+ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
To style scrollbars even inside of the shadow DOM, each rule needs to be prefixed with /deep/
. We use /deep/
instead of ::shadow
because we don't care about the selector of the host element in this case. We just want our styling to apply everywhere.
// With shadow DOM support using /deep/
+.scrollbars-visible-always {
+ /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ /deep/ ::-webkit-scrollbar-track,
+ /deep/ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ /deep/ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
The selector features discussed above allow you to target shadow DOM content with specific selectors, but Atom also allows you to target a specific shadow DOM context with an entire style sheet. The context into which a style sheet is loaded is based on the file name. If you want to load a style sheet into the editor, name it with the .atom-text-editor.less
or .atom-text-editor.css
extensions.
my-ui-theme/
+ styles/
+ index.less # loaded globally
+ index.atom-text-editor.less # loaded in the text editor shadow DOM
+
Inside a context-targeted style sheet, there's no need to use the ::shadow
or /deep/
expressions. If you want to refer to the element containing the shadow root, you can use the ::host
pseudo-element.
During the transition phase, style sheets targeting the atom-text-editor
context will also be loaded globally. Make sure you update your selectors in a way that maintains compatibility with the shadow DOM being disabled. That means if you use a ::host
pseudo element, you should also include the same style rule matches against atom-text-editor
.
You can now use the binary to link or install packages.
For example to install the ide-java
package from source:
# clone the repository and cd into it
+git clone https://github.com/pulsar-edit/ide-java
+cd ide-java
+
+# from the directory where you are running pulsar source code
+<pulsar source>/ppm/bin/apm link
+
In order to use version control in Atom, the project root needs to contain the Git repository.
The Alt+Cmd+ZAlt+Ctrl+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use Cmd+ZCtrl+Z afterwards to restore the previous contents.
You can configure Atom to be your Git commit editor with the following command:
$ git config --global core.editor "atom --wait"
+
This package also adds Alt+G Down and Alt+G Up keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
In order to use version control in Pulsar, the project root needs to contain the Git repository.
The LNX/WIN: Alt+Ctrl+Z - MAC: Alt+Cmd+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use LNX/WIN: Ctrl+Z - MAC: Cmd+Z afterwards to restore the previous contents.
$ git config --global core.editor "pulsar --wait"
+
This package also adds Alt+G Down and Alt+GUp keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
Atom's Safe Mode, which can be activated by completely exiting all instances of Atom and launching it again using the command atom --safe
from the command line, does the following:
~/.atom/packages
or ~/.atom/dev/packages
init.coffee
The intent of Safe Mode is to determine if a problem is being caused by a community package or is caused by built-in functionality of Atom. Disabling the init script was added because people tend to use the init script as a mini-package of sorts by adding code, commands and other functionality that would normally be in a package.
',4),m={href:"https://pulsar-edit.dev/docs/atom-archive/hacking-atom/#debugging",target:"_blank",rel:"noopener noreferrer"};function f(h,g){const a=c("ExternalLinkIcon");return n(),d("div",null,[l,o("p",null,[e("For more information on Safe Mode, check the "),o("a",m,[e("debugging section"),i(a)]),e(".")])])}const u=t(r,[["render",f],["__file","what-does-safe-mode-do.html.vue"]]);export{u as default}; diff --git a/assets/what-is-this-line-on-the-right-in-the-editor-view.html.92c5016f.js b/assets/what-is-this-line-on-the-right-in-the-editor-view.html.92c5016f.js new file mode 100644 index 0000000000..e2a2f08797 --- /dev/null +++ b/assets/what-is-this-line-on-the-right-in-the-editor-view.html.92c5016f.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-8a4db88a","path":"/docs/atom-archive/faq/sections/what-is-this-line-on-the-right-in-the-editor-view.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"What is this line on the right in the editor view?","slug":"what-is-this-line-on-the-right-in-the-editor-view","link":"#what-is-this-line-on-the-right-in-the-editor-view","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":2},{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.23,"words":70},"filePathRelative":"docs/atom-archive/faq/sections/what-is-this-line-on-the-right-in-the-editor-view.md"}');export{e as data}; diff --git a/assets/what-is-this-line-on-the-right-in-the-editor-view.html.e82e70ee.js b/assets/what-is-this-line-on-the-right-in-the-editor-view.html.e82e70ee.js new file mode 100644 index 0000000000..5c8dfab518 --- /dev/null +++ b/assets/what-is-this-line-on-the-right-in-the-editor-view.html.e82e70ee.js @@ -0,0 +1 @@ +import{_ as t}from"./wrap-guide-line.159c54bb.js";import{_ as i,o,c as n,a as e,b as h}from"./app.0e1565ce.js";const s={},a=e("h3",{id:"what-is-this-line-on-the-right-in-the-editor-view",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#what-is-this-line-on-the-right-in-the-editor-view","aria-hidden":"true"},"#"),h(" What is this line on the right in the editor view?")],-1),r=e("p",null,[e("img",{src:t,alt:"Wrap guide line"})],-1),d=e("p",null,"That's the wrap guide. It is a visual indicator of when your lines of code are getting too long. It defaults to the column that your Preferred Line Length is set to.",-1),c=e("p",null,"If you want to turn it off, you can disable the wrap-guide package in the Settings View.",-1),l=[a,r,d,c];function _(u,f){return o(),n("div",null,l)}const p=i(s,[["render",_],["__file","what-is-this-line-on-the-right-in-the-editor-view.html.vue"]]);export{p as default}; diff --git a/assets/what-platforms-does-atom-run-on.html.88de8aff.js b/assets/what-platforms-does-atom-run-on.html.88de8aff.js new file mode 100644 index 0000000000..c1a74cb8be --- /dev/null +++ b/assets/what-platforms-does-atom-run-on.html.88de8aff.js @@ -0,0 +1 @@ +import{_ as n,o as r,c as a,a as o,b as e,d as s,r as l}from"./app.0e1565ce.js";const i={},d=o("h3",{id:"what-platforms-does-atom-run-on",tabindex:"-1"},[o("a",{class:"header-anchor",href:"#what-platforms-does-atom-run-on","aria-hidden":"true"},"#"),e(" What platforms does Atom run on?")],-1),m=o("p",null,"Prebuilt versions of Atom are available for OS X 10.10 or later, Windows 7 or later, RedHat Linux, and Ubuntu Linux.",-1),c={href:"https://github.com/atom/atom/blob/master/README.md#building",target:"_blank",rel:"noopener noreferrer"};function u(f,_){const t=l("ExternalLinkIcon");return r(),a("div",null,[d,m,o("p",null,[e("If you would like to build from source on Windows, Linux, or OS X, see the "),o("a",c,[e("Atom README"),s(t)]),e(" for more information.")])])}const p=n(i,[["render",u],["__file","what-platforms-does-atom-run-on.html.vue"]]);export{p as default}; diff --git a/assets/what-platforms-does-atom-run-on.html.e67bfbea.js b/assets/what-platforms-does-atom-run-on.html.e67bfbea.js new file mode 100644 index 0000000000..86616f20f6 --- /dev/null +++ b/assets/what-platforms-does-atom-run-on.html.e67bfbea.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-39dbf018","path":"/docs/atom-archive/faq/sections/what-platforms-does-atom-run-on.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"What platforms does Atom run on?","slug":"what-platforms-does-atom-run-on","link":"#what-platforms-does-atom-run-on","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.19,"words":56},"filePathRelative":"docs/atom-archive/faq/sections/what-platforms-does-atom-run-on.md"}');export{t as data}; diff --git a/assets/what-s-the-difference-between-an-ide-and-an-editor.html.1f47c819.js b/assets/what-s-the-difference-between-an-ide-and-an-editor.html.1f47c819.js new file mode 100644 index 0000000000..fa8a32029e --- /dev/null +++ b/assets/what-s-the-difference-between-an-ide-and-an-editor.html.1f47c819.js @@ -0,0 +1 @@ +import{_ as a,o as r,c as i,a as o,b as e,d as n,r as s}from"./app.0e1565ce.js";const l={},d=o("h3",{id:"what-s-the-difference-between-an-ide-and-an-editor",tabindex:"-1"},[o("a",{class:"header-anchor",href:"#what-s-the-difference-between-an-ide-and-an-editor","aria-hidden":"true"},"#"),e(" What's the difference between an IDE and an editor?")],-1),h=o("p",null,[e('The term "IDE" comes from '),o("strong",null,"I"),e("ntegrated "),o("strong",null,"D"),e("evelopment "),o("strong",null,"E"),e("nvironment. It is intended as a set of tools that all work together: text editor, compiler, build or make integration, debugging, etc. Virtually all IDEs are tied specifically to a language or framework or tightly collected set of languages or frameworks. Some examples: Visual Studio for .NET and other Microsoft languages, RubyMine for Ruby, IntelliJ for Java, XCode for Apple technologies.")],-1),u={href:"https://daringfireball.net/projects/markdown/syntax",target:"_blank",rel:"noopener noreferrer"},c={href:"http://orgmode.org/",target:"_blank",rel:"noopener noreferrer"},f=o("p",null,[e("The tradeoff here is that while you can generally get off the ground faster if you're working within the realm of a given IDE, over the long term you spend a bunch of time retraining yourself when you inevitably change from one language or toolchain to the next. If you use an editor, you can continue to use the same workflows that you always have. Tools that you've built into your editor can be carried over to the next language and framework. Your editor becomes more powerful and more customized to how you want to work not just over years but potentially "),o("em",null,"decades"),e(". Just ask people who use vim or Emacs ... both of which have been available for over 25 years!")],-1),g=o("p",null,"So, if you want something that you can just jump into and be productive right away in a specific technology, perhaps an IDE is what you're looking for. If you want a tool that you can shape and customize into exactly what you want out of it even if it costs you some time up front configuring things, then an editor is probably more your speed \u26A1",-1);function m(y,p){const t=s("ExternalLinkIcon");return r(),i("div",null,[d,h,o("p",null,[e("An editor is simply that, a tool that is designed to edit text. Typically they are optimized for programming languages though many programmer's text editors are branching out and adding features for non-programming text like "),o("a",u,[e("Markdown"),n(t)]),e(" or "),o("a",c,[e("Org Mode"),n(t)]),e(". The key here is that text editors are designed to work with whatever language or framework you choose.")]),f,g])}const _=a(l,[["render",m],["__file","what-s-the-difference-between-an-ide-and-an-editor.html.vue"]]);export{_ as default}; diff --git a/assets/what-s-the-difference-between-an-ide-and-an-editor.html.7377822b.js b/assets/what-s-the-difference-between-an-ide-and-an-editor.html.7377822b.js new file mode 100644 index 0000000000..0eca1a3f73 --- /dev/null +++ b/assets/what-s-the-difference-between-an-ide-and-an-editor.html.7377822b.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-09b88655","path":"/docs/atom-archive/faq/sections/what-s-the-difference-between-an-ide-and-an-editor.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"What's the difference between an IDE and an editor?","slug":"what-s-the-difference-between-an-ide-and-an-editor","link":"#what-s-the-difference-between-an-ide-and-an-editor","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":1.13,"words":340},"filePathRelative":"docs/atom-archive/faq/sections/what-s-the-difference-between-an-ide-and-an-editor.md"}`);export{e as data}; diff --git a/assets/whitespace-settings.13b26c2a.js b/assets/whitespace-settings.13b26c2a.js new file mode 100644 index 0000000000..93416598c7 --- /dev/null +++ b/assets/whitespace-settings.13b26c2a.js @@ -0,0 +1 @@ +const s="/assets/whitespace-settings.7095448b.png";export{s as _}; diff --git a/assets/whitespace-settings.7095448b.png b/assets/whitespace-settings.7095448b.png new file mode 100644 index 0000000000..215182c374 Binary files /dev/null and b/assets/whitespace-settings.7095448b.png differ diff --git a/assets/whitespace.59413c09.png b/assets/whitespace.59413c09.png new file mode 100644 index 0000000000..e378c40ef1 Binary files /dev/null and b/assets/whitespace.59413c09.png differ diff --git a/assets/why-atom.html.4fb90fd5.js b/assets/why-atom.html.4fb90fd5.js new file mode 100644 index 0000000000..9d3a3d4ef4 --- /dev/null +++ b/assets/why-atom.html.4fb90fd5.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-60602147","path":"/docs/atom-archive/getting-started/sections/why-atom.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Why Atom?","slug":"why-atom","link":"#why-atom","children":[]}],"git":{"updatedTime":1663961800000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":2.41,"words":723},"filePathRelative":"docs/atom-archive/getting-started/sections/why-atom.md"}');export{t as data}; diff --git a/assets/why-atom.html.68b5b6aa.js b/assets/why-atom.html.68b5b6aa.js new file mode 100644 index 0000000000..164eeaa40e --- /dev/null +++ b/assets/why-atom.html.68b5b6aa.js @@ -0,0 +1 @@ +import{_ as e,o as t,c as a,f as o}from"./app.0e1565ce.js";const i={},n=o('There are a lot of text editors out there; why should you spend your time learning about and using Atom?
Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Atom to build Atom, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Atom is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local subprocesses was a non-starter.
For this reason, we didn't build Atom as a traditional web application. Instead, Atom is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Atom window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Atom, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Atom on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Atom as a perfect complement to GitHub's primary mission of building better software by working together. Atom is a long-term investment, and GitHub will continue to support its development with a dedicated team going forward. But we also know that we can't achieve our vision for Atom alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
The entire Atom editor is free and open source and is available under the https://github.com/atom organization.
',22),r=[n];function s(d,h){return t(),a("div",null,r)}const c=e(i,[["render",s],["__file","why-atom.html.vue"]]);export{c as default}; diff --git a/assets/why-does-atom-collect-usage-data.html.56dfada2.js b/assets/why-does-atom-collect-usage-data.html.56dfada2.js new file mode 100644 index 0000000000..b02327fd5a --- /dev/null +++ b/assets/why-does-atom-collect-usage-data.html.56dfada2.js @@ -0,0 +1 @@ +import{_ as t,o as a,c as o,a as e,b as s}from"./app.0e1565ce.js";const i={},n=e("h3",{id:"why-does-atom-collect-usage-data",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#why-does-atom-collect-usage-data","aria-hidden":"true"},"#"),s(" Why does Atom collect usage data?")],-1),c=e("p",null,"In the same way that aggregate usage information is important when developing a web application, we've found that it's just as important for desktop applications. By knowing which Atom features are being used the most, and how the editor is performing, we can focus our development efforts in the right place. For details on what data Atom is sending or to learn how to disable metrics gathering, visit https://github.com/atom/metrics.",-1),r=[n,c];function d(h,l){return a(),o("div",null,r)}const u=t(i,[["render",d],["__file","why-does-atom-collect-usage-data.html.vue"]]);export{u as default}; diff --git a/assets/why-does-atom-collect-usage-data.html.fd1638d4.js b/assets/why-does-atom-collect-usage-data.html.fd1638d4.js new file mode 100644 index 0000000000..1bd8d98821 --- /dev/null +++ b/assets/why-does-atom-collect-usage-data.html.fd1638d4.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-616d7939","path":"/docs/atom-archive/faq/sections/why-does-atom-collect-usage-data.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Why does Atom collect usage data?","slug":"why-does-atom-collect-usage-data","link":"#why-does-atom-collect-usage-data","children":[]}],"git":{"updatedTime":1667690985000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1},{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":0.27,"words":80},"filePathRelative":"docs/atom-archive/faq/sections/why-does-atom-collect-usage-data.md"}');export{e as data}; diff --git a/assets/why-does-macos-say-that-atom-wants-to-access-my-calendar-contacts-photos-etc.html.d4b59bf7.js b/assets/why-does-macos-say-that-atom-wants-to-access-my-calendar-contacts-photos-etc.html.d4b59bf7.js new file mode 100644 index 0000000000..afbd92edbd --- /dev/null +++ b/assets/why-does-macos-say-that-atom-wants-to-access-my-calendar-contacts-photos-etc.html.d4b59bf7.js @@ -0,0 +1 @@ +import{_ as c,o as n,c as i,a as t,b as e,d as a,f as s,r}from"./app.0e1565ce.js";const h={},l=s('With macOS 10.14 Mojave, Apple introduced new privacy protections similar to the existing protections found in iOS. Whenever an application attempts to access the files inside certain newly-protected directories, macOS asks the user whether they want to allow the application to access the content in those directories. These new privacy protections apply to the directories that contain your calendars, contacts, photos, mail, messages, and Time Machine backups.
Applications trigger these new macOS prompts when attempting to access these directories in any way. Simply attempting to list the files in one of these directories is enough to trigger these prompts. These protections even apply to Apple's own applications. For example, if you open Terminal.app
and try to list the files in ~/Library/Calendars
, macOS shows a prompt saying, '"Terminal" would like access to your calendar.'
In addition to containing the files you're intending to edit inside Atom, your home directory also contains your files that have new OS-level protections in Mojave:
~/Library/Calendars
)~/Library/Application\\ Support/AddressBook
~/Library/Mail
)~/Pictures/Photos\\ Library.photoslibrary
)Before letting Atom read these files, Mojave is understandably asking whether you want Atom to be able to access this personal data.
Most people don't use Atom to view or edit their calendar files, contact files, photo library, etc. If you don't intend to use Atom to view/edit these files, then Atom doesn't need access to them. If you see a prompt from macOS saying that Atom would like to access these items, simply click Don't Allow.
To Atom, these items are just files on disk. Atom treats them exactly like any other file you would view in Atom. Therefore, if you allow Atom to access these items, you'll be able to use Atom to browse the directories that contain these items, and you'll be able to view the files in those directories. That's it. Nothing more.
Fortunately, macOS will only prompt you once for each type of personal data. In other words, you might see a prompt asking you whether Atom can access your calendar, and you might see a prompt asking you whether Atom can access your contacts, but once you make those decisions, you won't see those prompts again.
At any time, you can change your choices via System Preferences. Inside System Preferences, go to Security and Privacy
, click the Privacy
tab, and then click on Calendars
to manage which apps can access your Calendars
. The same goes for Contacts
, Photos
, etc.:
Many people understandably expect their text editor to be able to open any file on disk. And that's exactly how things worked prior to macOS Mojave. If you would like to restore that behavior, you can proactively instruct macOS to allow you to access all files with Atom. To do so:
Applications
folder in the FinderSecurity and Privacy
icon, click the Privacy
tab, and then click on Full Disk Access
in the left-hand sidebarFull Disk Access
as shown belowThere are a lot of text editors out there; why should you spend your time learning about and using Pulsar? Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Pulsar to build Pulsar, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Pulsar is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local sub-processes was a non-starter.
For this reason, we didn't build Pulsar as a traditional web application. Instead, Pulsar is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Pulsar window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Pulsar, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Pulsar on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Pulsar as a great replacement for Atom but we can't do it without your support going forward, since we know that we can't achieve our vision for Pulsar alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
',19),p={href:"https://github.com/pulsar-edit",target:"_blank",rel:"noopener noreferrer"};function w(b,m){const a=l("ExternalLinkIcon");return r(),i("div",null,[u,n('TODO: We probably want to make this our own, it is very "Atom" still'),c,t("p",null,[e("The entire Pulsar editor is free and open source and available under our "),t("a",p,[e("Organizational"),s(a)]),e(" repositories.")])])}const g=o(h,[["render",w],["__file","why-pulsar.html.vue"]]);export{g as default}; diff --git a/assets/why-pulsar.html.62b37d90.js b/assets/why-pulsar.html.62b37d90.js new file mode 100644 index 0000000000..bc22538c9a --- /dev/null +++ b/assets/why-pulsar.html.62b37d90.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1ca4faf8","path":"/docs/launch-manual/sections/getting-started/sections/why-pulsar.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Why Pulsar?","slug":"why-pulsar","link":"#why-pulsar","children":[{"level":3,"title":"The Nucleus of Pulsar","slug":"the-nucleus-of-pulsar","link":"#the-nucleus-of-pulsar","children":[]},{"level":3,"title":"The Native Web","slug":"the-native-web","link":"#the-native-web","children":[]},{"level":3,"title":"JavaScript, Meet C++","slug":"javascript-meet-c","link":"#javascript-meet-c","children":[]},{"level":3,"title":"Web Tech: The Fun Parts","slug":"web-tech-the-fun-parts","link":"#web-tech-the-fun-parts","children":[]},{"level":3,"title":"An Open-Source Text Editor","slug":"an-open-source-text-editor","link":"#an-open-source-text-editor","children":[]}]}],"git":{"updatedTime":1670961579000,"contributors":[{"name":"Daeraxa","email":"58074586+Daeraxa@users.noreply.github.com","commits":1}]},"readingTime":{"minutes":2.4,"words":719},"filePathRelative":"docs/launch-manual/sections/getting-started/sections/why-pulsar.md"}');export{e as data}; diff --git a/assets/windows-downloads.16f9b58b.png b/assets/windows-downloads.16f9b58b.png new file mode 100644 index 0000000000..333ec33af3 Binary files /dev/null and b/assets/windows-downloads.16f9b58b.png differ diff --git a/assets/windows-downloads.e9b59e10.js b/assets/windows-downloads.e9b59e10.js new file mode 100644 index 0000000000..bca64f0f0b --- /dev/null +++ b/assets/windows-downloads.e9b59e10.js @@ -0,0 +1 @@ +const s="/assets/linux-downloads.66db3662.png",o="/assets/mac-downloads.a1802ab2.png",a="/assets/windows-downloads.16f9b58b.png";export{s as _,o as a,a as b}; diff --git a/assets/windows-system-settings.141561e2.js b/assets/windows-system-settings.141561e2.js new file mode 100644 index 0000000000..af1cf53491 --- /dev/null +++ b/assets/windows-system-settings.141561e2.js @@ -0,0 +1 @@ +const s="/assets/windows-system-settings.9698827f.png";export{s as _}; diff --git a/assets/windows-system-settings.9698827f.png b/assets/windows-system-settings.9698827f.png new file mode 100644 index 0000000000..74feaf948a Binary files /dev/null and b/assets/windows-system-settings.9698827f.png differ diff --git a/assets/wordcount.cf347e3c.png b/assets/wordcount.cf347e3c.png new file mode 100644 index 0000000000..c29e770805 Binary files /dev/null and b/assets/wordcount.cf347e3c.png differ diff --git a/assets/wrap-guide-line.159c54bb.js b/assets/wrap-guide-line.159c54bb.js new file mode 100644 index 0000000000..9693bf0353 --- /dev/null +++ b/assets/wrap-guide-line.159c54bb.js @@ -0,0 +1 @@ +const s="/assets/wrap-guide-line.efbabdc6.png";export{s as _}; diff --git a/assets/wrap-guide-line.efbabdc6.png b/assets/wrap-guide-line.efbabdc6.png new file mode 100644 index 0000000000..eea72fc74d Binary files /dev/null and b/assets/wrap-guide-line.efbabdc6.png differ diff --git a/assets/writing-in-atom.html.1d45f779.js b/assets/writing-in-atom.html.1d45f779.js new file mode 100644 index 0000000000..bb666f049c --- /dev/null +++ b/assets/writing-in-atom.html.1d45f779.js @@ -0,0 +1,4 @@ +import{_ as s,a as i}from"./preview.2eaf146a.js";import{_ as r,o as l,c,a as t,b as e,d as o,f as n,r as p}from"./app.0e1565ce.js";const d={},h=t("h3",{id:"writing-in-atom",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#writing-in-atom","aria-hidden":"true"},"#"),e(" Writing in Atom")],-1),u={href:"https://help.github.com/articles/about-writing-and-formatting-on-github/",target:"_blank",rel:"noopener noreferrer"},m=n('In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Atom will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting Cmd+Shift+;Ctrl+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Atom will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Atom ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press Cmd+CCtrl+CCtrl+Ins or if you right-click in the preview pane and choose "Copy as HTML".
',6),g={href:"https://github.com/atom/markdown-preview",target:"_blank",rel:"noopener noreferrer"},w=n(`There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing "Snippets: Available" in the Command Palette.
In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Pulsar will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting LNX/WIN: Ctrl+Shift+; - MAC: Cmd+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Pulsar will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Pulsar ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press LNX: Ctrl+Ins - WIN: Ctrl+C - MAC: Cmd+C or if you right-click in the preview pane and choose "Copy as HTML".
',6),f={href:"https://github.com/pulsar-edit/markdown-preview",target:"_blank",rel:"noopener noreferrer"},b=n(`There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing Snippets: Available
in the Command Palette.
Spec files must end with -spec
so add sample-spec.js
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Pulsar includes the following:
`,3),w={href:"https://github.com/velesin/jasmine-jquery",target:"_blank",rel:"noopener noreferrer"},y=e("toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domWriting Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Pulsar. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Pulsar core specs when working on Pulsar itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
To run tests on the command line, run Pulsar with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
pulsar --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Pulsar will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Pulsar will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Pulsar's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.pulsar
).enablePersistence
A boolean indicating whether the Pulsar environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via pulsar --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finished running. This exit code will be returned when running your tests via the command line.
`,8);function E(C,F){const a=i("ExternalLinkIcon");return p(),c("div",null,[l,r,s("p",null,[n("Pulsar uses "),s("a",d,[n("Jasmine"),t(a)]),n(" as its spec framework. Any new functionality should have specs to guard against regressions.")]),k,s("p",null,[s("a",h,[n("Pulsar specs"),t(a)]),n(" and "),s("a",m,[n("package specs"),t(a)]),n(" are added to their respective "),v,n(" directory. The example below creates a spec for Pulsar core.")]),f,s("p",null,[n("The best way to learn about expectations is to read the "),s("a",b,[n("Jasmine documentation"),t(a)]),n(" about them. Below is a simple example.")]),g,s("ul",null,[s("li",null,[s("a",w,[n("jasmine-jquery"),t(a)])]),y]),s("p",null,[n("These are defined in "),s("a",q,[n("spec/spec-helper.js"),t(a)]),n(".")]),x,s("p",null,[n("For a more detailed documentation on asynchronous tests please visit the "),s("a",_,[n("Jasmine documentation"),t(a)]),n(".")]),j,s("p",null,[n("It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the "),s("a",P,[n("Travis CI For Your Packages"),t(a)]),n(" and "),s("a",A,[n("AppVeyor CI For Your Packages"),t(a)]),n(" posts for more details.")]),T])}const I=o(u,[["render",E],["__file","writing-specs.html.vue"]]);export{I as default}; diff --git a/assets/writing-specs.html.ccd20a54.js b/assets/writing-specs.html.ccd20a54.js new file mode 100644 index 0000000000..67a1713c07 --- /dev/null +++ b/assets/writing-specs.html.ccd20a54.js @@ -0,0 +1,89 @@ +import{_ as o,o as p,c,a as s,b as n,d as t,f as e,r as i}from"./app.0e1565ce.js";const u={},l=s("h3",{id:"writing-specs",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#writing-specs","aria-hidden":"true"},"#"),n(" Writing Specs")],-1),r=s("p",null,"We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Atom?",-1),d={href:"https://jasmine.github.io/archives/1.3/introduction",target:"_blank",rel:"noopener noreferrer"},k=s("h4",{id:"create-a-new-spec",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#create-a-new-spec","aria-hidden":"true"},"#"),n(" Create a New Spec")],-1),h={href:"https://github.com/atom/atom/tree/master/spec",target:"_blank",rel:"noopener noreferrer"},m={href:"https://github.com/atom/markdown-preview/tree/master/spec",target:"_blank",rel:"noopener noreferrer"},v=s("code",null,"spec",-1),f=e(`Spec files must end with -spec
so add sample-spec.coffee
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Atom includes the following:
`,3),w={href:"https://github.com/velesin/jasmine-jquery",target:"_blank",rel:"noopener noreferrer"},y=e("toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domWriting Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Atom. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Atom core specs when working on Atom itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
To run tests on the command line, run Atom with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
atom --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
WARNING
Warning: This API is available as of 1.2.0-beta0, and it is experimental and subject to change. Test runner authors should be prepared to test their code against future beta releases until it stabilizes.
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Atom will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Atom will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Atom's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.atom
).enablePersistence
A boolean indicating whether the Atom environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via atom --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finish running. This exit code will be returned when running your tests via the command line.
`,9);function E(C,F){const a=i("ExternalLinkIcon");return p(),c("div",null,[l,r,s("p",null,[n("Atom uses "),s("a",d,[n("Jasmine"),t(a)]),n(" as its spec framework. Any new functionality should have specs to guard against regressions.")]),k,s("p",null,[s("a",h,[n("Atom specs"),t(a)]),n(" and "),s("a",m,[n("package specs"),t(a)]),n(" are added to their respective "),v,n(" directory. The example below creates a spec for Atom core.")]),f,s("p",null,[n("The best way to learn about expectations is to read the "),s("a",b,[n("Jasmine documentation"),t(a)]),n(" about them. Below is a simple example.")]),g,s("ul",null,[s("li",null,[s("a",w,[n("jasmine-jquery"),t(a)])]),y]),s("p",null,[n("These are defined in "),s("a",q,[n("spec/spec-helper.coffee"),t(a)]),n(".")]),x,s("p",null,[n("For a more detailed documentation on asynchronous tests please visit the "),s("a",_,[n("Jasmine documentation"),t(a)]),n(".")]),j,s("p",null,[n("It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the "),s("a",A,[n("Travis CI For Your Packages"),t(a)]),n(" and "),s("a",T,[n("AppVeyor CI For Your Packages"),t(a)]),n(" posts for more details.")]),P])}const I=o(u,[["render",E],["__file","writing-specs.html.vue"]]);export{I as default}; diff --git a/assets/writing-specs.html.de9c57a1.js b/assets/writing-specs.html.de9c57a1.js new file mode 100644 index 0000000000..4548042139 --- /dev/null +++ b/assets/writing-specs.html.de9c57a1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-f2fe7262","path":"/docs/atom-archive/hacking-atom/sections/writing-specs.html","title":"","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":3,"title":"Writing Specs","slug":"writing-specs","link":"#writing-specs","children":[]}],"git":{"updatedTime":1664050274000,"contributors":[{"name":"Josh","email":"16845458+kaosine@users.noreply.github.com","commits":3}]},"readingTime":{"minutes":4.62,"words":1385},"filePathRelative":"docs/atom-archive/hacking-atom/sections/writing-specs.md"}');export{e as data}; diff --git a/assets/zoom.50f0dc7b.js b/assets/zoom.50f0dc7b.js new file mode 100644 index 0000000000..3ec157a9d4 --- /dev/null +++ b/assets/zoom.50f0dc7b.js @@ -0,0 +1 @@ +const s="/assets/zoom.bfb256f6.png";export{s as _}; diff --git a/assets/zoom.bfb256f6.png b/assets/zoom.bfb256f6.png new file mode 100644 index 0000000000..25bea0edb8 Binary files /dev/null and b/assets/zoom.bfb256f6.png differ diff --git a/blog/20221112-Daeraxa-ExamplePost.html b/blog/20221112-Daeraxa-ExamplePost.html new file mode 100644 index 0000000000..434f04b7d1 --- /dev/null +++ b/blog/20221112-Daeraxa-ExamplePost.html @@ -0,0 +1,223 @@ + + + + + + + +STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
This appendix covers the the Atom server-side APIs that various parts of Atom consume.
WARNING
Warning: These APIs should be considered pre-release and are subject to change.
This guide describes the web API used by apm and Atom. The vast majority of use cases are met by the apm
command-line tool, which does other useful things like incrementing your version in package.json
and making sure you have pushed your git tag. In fact, Atom itself shells out to apm
rather than hitting the API directly. If you're curious about how Atom uses apm
, see the PackageManager class in the settings-view
package.
WARNING
Warning: This API should be considered pre-release and is subject to change.
For calls to the API that require authentication, provide a valid token from your atom.io account page in the Authorization
header.
All requests that take parameters require application/json
.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to downloads
asc
or desc
. Defaults to desc
. stars
can only be ordered desc
Returns a list of all packages in the following format:
[
+ {
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ }
+ },
+ ...
+ ]
+
Results are paginated 30 at a time, and links to the next and last pages are provided in the Link
header:
Link: <https://www.atom.io/api/packages?page=1>; rel="self",
+ <https://www.atom.io/api/packages?page=41>; rel="last",
+ <https://www.atom.io/api/packages?page=2>; rel="next"
+
By default, results are sorted by download count, descending.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to the relevance of the search query.asc
or desc
. Defaults to desc
.Returns results in the same format as listing packages.
Returns package details and versions for a single package
Parameters:
Returns:
{
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ },
+ "versions": [
+ (see single version output below)
+ ...,
+ ]
+ }
+
Create a new package; requires authentication.
The name and version will be fetched from the package.json
file in the specified repository. The authenticating user must have access to the indicated repository.
Parameters:
Returns:
Delete a package; requires authentication.
Returns:
Packages are renamed by publishing a new version with the name changed in package.json
. See Creating a new package version for details.
Requests made to the previous name will forward to the new name.
Returns package.json
with dist
key added for e.g. tarball download:
{
+ "bugs": {
+ "url": "https://github.com/thedaniel/test-package/issues"
+ },
+ "dependencies": {
+ "async": "~0.2.6",
+ "pegjs": "~0.7.0",
+ "season": "~0.13.0"
+ },
+ "description": "Expand snippets matching the current prefix with `tab`.",
+ "dist": {
+ "tarball": "https://codeload.github.com/..."
+ },
+ "engines": {
+ "atom": "*"
+ },
+ "main": "./lib/snippets",
+ "name": "thedaniel-test-package",
+ "publishConfig": {
+ "registry": "https://..."
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package.git"
+ },
+ "version": "0.6.0"
+}
+
Creates a new package version from a git tag; requires authentication. If rename
is not true
, the name
field in package.json
must match the current package name.
Parameters:
version
key in the package.json
file at that ref. The authenticating user must have access to the package repository.Returns:
Deletes a package version; requires authentication.
Note that a version cannot be republished with a different tag if it is deleted. If you need to delete the latest version of a package for example for security reasons, you'll need to increment the version when republishing.
Returns 204 No Content
List a user's starred packages.
Return value is similar to GET /api/packages
List the authenticated user's starred packages; requires authentication.
Return value is similar to GET /api/packages
Star a package; requires authentication.
Returns a package.
Unstar a package; requires authentication.
Returns 204 No Content.
List the users that have starred a package.
Returns a list of user objects:
[{ "login": "aperson" }, { "login": "anotherperson" }]
+
WARNING
Warning: This API should be considered pre-release and is subject to change.
Atom update feed, following the format expected by Squirrel.
Returns:
{
+ "name": "0.96.0",
+ "notes": "[HTML release notes]",
+ "pub_date": "2014-05-19T15:52:06.000Z",
+ "url": "https://www.atom.io/api/updates/download"
+}
+
This guide describes the web API used by apm and Atom. The vast majority of use cases are met by the apm
command-line tool, which does other useful things like incrementing your version in package.json
and making sure you have pushed your git tag. In fact, Atom itself shells out to apm
rather than hitting the API directly. If you're curious about how Atom uses apm
, see the PackageManager class in the settings-view
package.
WARNING
Warning: This API should be considered pre-release and is subject to change.
For calls to the API that require authentication, provide a valid token from your atom.io account page in the Authorization
header.
All requests that take parameters require application/json
.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to downloads
asc
or desc
. Defaults to desc
. stars
can only be ordered desc
Returns a list of all packages in the following format:
[
+ {
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ }
+ },
+ ...
+ ]
+
Results are paginated 30 at a time, and links to the next and last pages are provided in the Link
header:
Link: <https://www.atom.io/api/packages?page=1>; rel="self",
+ <https://www.atom.io/api/packages?page=41>; rel="last",
+ <https://www.atom.io/api/packages?page=2>; rel="next"
+
By default, results are sorted by download count, descending.
Parameters:
downloads
, created_at
, updated_at
, stars
. Defaults to the relevance of the search query.asc
or desc
. Defaults to desc
.Returns results in the same format as listing packages.
Returns package details and versions for a single package
Parameters:
Returns:
{
+ "releases": {
+ "latest": "0.6.0"
+ },
+ "name": "thedaniel-test-package",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package"
+ },
+ "versions": [
+ (see single version output below)
+ ...,
+ ]
+ }
+
Create a new package; requires authentication.
The name and version will be fetched from the package.json
file in the specified repository. The authenticating user must have access to the indicated repository.
Parameters:
Returns:
Delete a package; requires authentication.
Returns:
Packages are renamed by publishing a new version with the name changed in package.json
. See Creating a new package version for details.
Requests made to the previous name will forward to the new name.
Returns package.json
with dist
key added for e.g. tarball download:
{
+ "bugs": {
+ "url": "https://github.com/thedaniel/test-package/issues"
+ },
+ "dependencies": {
+ "async": "~0.2.6",
+ "pegjs": "~0.7.0",
+ "season": "~0.13.0"
+ },
+ "description": "Expand snippets matching the current prefix with `tab`.",
+ "dist": {
+ "tarball": "https://codeload.github.com/..."
+ },
+ "engines": {
+ "atom": "*"
+ },
+ "main": "./lib/snippets",
+ "name": "thedaniel-test-package",
+ "publishConfig": {
+ "registry": "https://..."
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thedaniel/test-package.git"
+ },
+ "version": "0.6.0"
+}
+
Creates a new package version from a git tag; requires authentication. If rename
is not true
, the name
field in package.json
must match the current package name.
Parameters:
version
key in the package.json
file at that ref. The authenticating user must have access to the package repository.Returns:
Deletes a package version; requires authentication.
Note that a version cannot be republished with a different tag if it is deleted. If you need to delete the latest version of a package for example for security reasons, you'll need to increment the version when republishing.
Returns 204 No Content
List a user's starred packages.
Return value is similar to GET /api/packages
List the authenticated user's starred packages; requires authentication.
Return value is similar to GET /api/packages
Star a package; requires authentication.
Returns a package.
Unstar a package; requires authentication.
Returns 204 No Content.
List the users that have starred a package.
Returns a list of user objects:
[{ "login": "aperson" }, { "login": "anotherperson" }]
+
WARNING
Warning: This API should be considered pre-release and is subject to change.
Atom update feed, following the format expected by Squirrel.
Returns:
{
+ "name": "0.96.0",
+ "notes": "[HTML release notes]",
+ "pub_date": "2014-05-19T15:52:06.000Z",
+ "url": "https://www.atom.io/api/updates/download"
+}
+
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
Now that we've written a number of packages and themes, let's take minute to take a closer look at some of the ways that Atom works in greater depth. Here we'll go into more of a deep dive on individual internal APIs and systems of Atom, even looking at some Atom source to see how things are really getting done.
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with `config.get`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
Subscription methods return Disposable
objects that can be used to unsubscribe. Note in the example above how we save the subscription to the @fontSizeObserveSubscription
instance variable and dispose of it when the view is detached. To group multiple subscriptions together, you can add them all to a CompositeDisposable
that you dispose when the view is detached.
The atom.config
database is populated on startup from ~/.atom/config.cson
%USERPROFILE%\.atom\config.cson
, but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
If you're exposing package configuration via specific key paths, you'll want to associate them with a schema in your package's main module. Read more about schemas in the Config API documentation.
Keymap files are encoded as JSON or CSON files containing nested hashes. They work much like style sheets, but instead of applying style properties to elements matching the selector, they specify the meaning of keystrokes on elements matching the selector. Here is an example of some bindings that apply when keystrokes pass through atom-text-editor
elements:
Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and Alt+BackspaceCtrl+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (Cmd+Shift+PCtrl+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Atom, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.coffee
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for Cmd+OCtrl+O when the selection is inside an editor pane:
::: codetabs#keymaps-in-depth
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
'atom-text-editor':
+ 'cmd-o': 'abort!'
+
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
:::
But if you click inside the Tree View and press Cmd+OCtrl+O, it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.Sometimes the problem isn't mapping the command to a key combination, the problem is that Atom doesn't recognize properly what keys you're pressing. This is due to some limitations in how Chromium reports keyboard events. But even this can be customized now.
You can add the following to your init.coffee
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
Or if you've converted your init script to JavaScript:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
If you want to know the event
for the keystroke you pressed you can paste the following script to your developer tools console
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Atom to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
Atom supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
Config::set
accepts a scopeSelector
. If you'd like to set a setting for JavaScript function names, you can give it the JavaScript function name scopeSelector
:
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
A scope descriptor is an Object that wraps an Array
of String
s. The Array describes a path from the root of the syntax tree to a token including all scope names for the entire path.
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
Config::get
accepts a scopeDescriptor
. You can get the value for your setting scoped to JavaScript function names via:
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
Editor::getRootScopeDescriptor
to get the language's descriptor. For example: [".source.js"]
Editor::scopeDescriptorForBufferPosition
to get the descriptor at a specific position in the buffer.Cursor::getScopeDescriptor
to get a cursor's descriptor based on position. eg. if the cursor were in the name of the method in our example it would return ["source.js", "meta.function.js", "entity.name.function.js"]
Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer—the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Atom to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Atom will only attempt to call deserialize if the two versions match, and otherwise return undefined. We plan on implementing a migration system in the future, but this at least protects you from improperly deserializing old state.
Atom contains a number of packages that are Node modules instead of Atom packages. If you want to make changes to the Node modules, for instance atom-keymap
, you have to link them into the development environment differently than you would a normal Atom package.
Here are the steps to run a local version of a Node module within Atom. We're using atom-keymap
as an example:
::: tab#developing-node-modules
$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than ~/github/atom
+# you need to set the following environment variable
+$ export ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than %USERPROFILE%\github\atom
+# you need to set the following environment variable
+$ setx ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
:::
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Atom and do the following:
$ cd <em>WHERE YOU CLONED THE NODE MODULE</em>
+$ npm install
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ apm rebuild
+$ atom --dev .
+
Atom packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
Similarly, to consume a service, specify one or more version ranges, each paired with the name of a method on the package's main module:
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
Warning
Danger: 🚨 Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the apm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if apm has taken care of things for you.
Note
Note: The apm tool will only publish and https://atom.io will only list packages that are hosted on GitHub, regardless of what process is used to publish them.
When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\d+\.\d+\.\d+
^v\d+\.\d+\.\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
apm publish --tag tagname
where tagname
must match the name of the tag created in the above stepSome packages get too big for one person. Sometimes priorities change and someone else wants to help out. You can let others help or create co-owners by adding them as a collaborator on the GitHub repository for your package. Note: Anyone that has push access to your repository will have the ability to publish new versions of the package that belongs to that repository.
You can also have packages that are owned by a GitHub organization. Anyone who is a member of an organization's team which has push access to the package's repository will be able to publish new versions of the package.
Warning
Danger: 🚨 This is a permanent change. There is no going back! 🚨
If you want to hand off support of your package to someone else, you can do that by transferring the package's repository to the new owner. Once you do that, they can publish a new version with the updated repository information in the package.json
.
If you no longer want to support your package and cannot find anyone to take it over, you can unpublish your package from https://atom.io. For example, if your package is named package-name
then the command you would execute is:
$ apm unpublish <em>package-name</em>
+
This will remove your package from the https://atom.io package registry. Anyone who has already downloaded a copy of your package will still have it and be able to use it, but it will no longer be available for installation by others.
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ apm unpublish <em>package-name@1.2.3</em>
+
This will remove just this particular version from the https://atom.io package registry.
If you need to rename your package for any reason, you can do so with one simple command – apm publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ apm publish --rename <em>new-package-name</em>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
In order to improve startup time, when Atom is built we create a V8 snapshot in which we preload core services and packages. Then, at runtime, we finish loading Atom by supplying all the information we didn't have during the compilation phase (e.g. loading third party packages, custom style sheets, configuration, etc.).
electron-link is the tool that powers snapshots, as it enables us to traverse the entire require graph (starting at the entry point) and replace all the forbidden require
calls (e.g. require calls to native modules, node core modules or other modules that can't be accessed in the snapshot V8 context) with a function that will be called at runtime. When adding new code to Atom, we always try to put it inside the snapshot by, for example, deferring the usage of DOM APIs or native node modules to a later moment in time when those facilities are available. If that is not possible, we will add the unsupported code paths to the list of files that get excluded from the snapshot, ensuring we only exclude those ones that are not supported as opposed to skipping an entire Node module.
The output of electron-link is a single script containing the code for all the modules reachable from the entry point, which we then supply to mksnapshot to generate a snapshot blob.
The generated blob is finally copied into the application bundle and will be automatically loaded by Electron when running Atom.
You should now have a better understanding of some of the core Atom APIs and systems.
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with `config.get`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
Subscription methods return Disposable
objects that can be used to unsubscribe. Note in the example above how we save the subscription to the @fontSizeObserveSubscription
instance variable and dispose of it when the view is detached. To group multiple subscriptions together, you can add them all to a CompositeDisposable
that you dispose when the view is detached.
The atom.config
database is populated on startup from ~/.atom/config.cson
%USERPROFILE%\.atom\config.cson
, but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
If you're exposing package configuration via specific key paths, you'll want to associate them with a schema in your package's main module. Read more about schemas in the Config API documentation.
Atom contains a number of packages that are Node modules instead of Atom packages. If you want to make changes to the Node modules, for instance atom-keymap
, you have to link them into the development environment differently than you would a normal Atom package.
Here are the steps to run a local version of a Node module within Atom. We're using atom-keymap
as an example:
::: tab#developing-node-modules
$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than ~/github/atom
+# you need to set the following environment variable
+$ export ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
$ git clone https://github.com/atom/atom-keymap.git
+$ cd atom-keymap
+$ npm install
+$ npm link
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ npm link atom-keymap
+
+# This is the special step, it makes the Node module work with Atom's version of Node
+$ apm rebuild
+
+# If you have cloned Atom in a different location than %USERPROFILE%\github\atom
+# you need to set the following environment variable
+$ setx ATOM_DEV_RESOURCE_PATH=<em>WHERE YOU CLONED ATOM</em>
+
+# Should work!
+$ atom --dev .
+
:::
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Atom and do the following:
$ cd <em>WHERE YOU CLONED THE NODE MODULE</em>
+$ npm install
+$ cd <em>WHERE YOU CLONED ATOM</em>
+$ apm rebuild
+$ atom --dev .
+
In order to improve startup time, when Atom is built we create a V8 snapshot in which we preload core services and packages. Then, at runtime, we finish loading Atom by supplying all the information we didn't have during the compilation phase (e.g. loading third party packages, custom style sheets, configuration, etc.).
electron-link is the tool that powers snapshots, as it enables us to traverse the entire require graph (starting at the entry point) and replace all the forbidden require
calls (e.g. require calls to native modules, node core modules or other modules that can't be accessed in the snapshot V8 context) with a function that will be called at runtime. When adding new code to Atom, we always try to put it inside the snapshot by, for example, deferring the usage of DOM APIs or native node modules to a later moment in time when those facilities are available. If that is not possible, we will add the unsupported code paths to the list of files that get excluded from the snapshot, ensuring we only exclude those ones that are not supported as opposed to skipping an entire Node module.
The output of electron-link is a single script containing the code for all the modules reachable from the entry point, which we then supply to mksnapshot to generate a snapshot blob.
The generated blob is finally copied into the application bundle and will be automatically loaded by Electron when running Atom.
Atom packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
Similarly, to consume a service, specify one or more version ranges, each paired with the name of a method on the package's main module:
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
Keymap files are encoded as JSON or CSON files containing nested hashes. They work much like style sheets, but instead of applying style properties to elements matching the selector, they specify the meaning of keystrokes on elements matching the selector. Here is an example of some bindings that apply when keystrokes pass through atom-text-editor
elements:
Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and Alt+BackspaceCtrl+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (Cmd+Shift+PCtrl+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Atom, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.coffee
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for Cmd+OCtrl+O when the selection is inside an editor pane:
::: codetabs#keymaps-in-depth
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
'atom-text-editor':
+ 'cmd-o': 'abort!'
+
'atom-text-editor':
+ 'ctrl-o': 'abort!'
+
:::
But if you click inside the Tree View and press Cmd+OCtrl+O, it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.Sometimes the problem isn't mapping the command to a key combination, the problem is that Atom doesn't recognize properly what keys you're pressing. This is due to some limitations in how Chromium reports keyboard events. But even this can be customized now.
You can add the following to your init.coffee
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
Or if you've converted your init script to JavaScript:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
If you want to know the event
for the keystroke you pressed you can paste the following script to your developer tools console
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Atom to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
Warning
Danger: 🚨 Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the apm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if apm has taken care of things for you.
Note
Note: The apm tool will only publish and https://atom.io will only list packages that are hosted on GitHub, regardless of what process is used to publish them.
When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\d+\.\d+\.\d+
^v\d+\.\d+\.\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
apm publish --tag tagname
where tagname
must match the name of the tag created in the above stepSome packages get too big for one person. Sometimes priorities change and someone else wants to help out. You can let others help or create co-owners by adding them as a collaborator on the GitHub repository for your package. Note: Anyone that has push access to your repository will have the ability to publish new versions of the package that belongs to that repository.
You can also have packages that are owned by a GitHub organization. Anyone who is a member of an organization's team which has push access to the package's repository will be able to publish new versions of the package.
Warning
Danger: 🚨 This is a permanent change. There is no going back! 🚨
If you want to hand off support of your package to someone else, you can do that by transferring the package's repository to the new owner. Once you do that, they can publish a new version with the updated repository information in the package.json
.
If you no longer want to support your package and cannot find anyone to take it over, you can unpublish your package from https://atom.io. For example, if your package is named package-name
then the command you would execute is:
$ apm unpublish <em>package-name</em>
+
This will remove your package from the https://atom.io package registry. Anyone who has already downloaded a copy of your package will still have it and be able to use it, but it will no longer be available for installation by others.
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ apm unpublish <em>package-name@1.2.3</em>
+
This will remove just this particular version from the https://atom.io package registry.
If you need to rename your package for any reason, you can do so with one simple command – apm publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ apm publish --rename <em>new-package-name</em>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
Atom supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
Config::set
accepts a scopeSelector
. If you'd like to set a setting for JavaScript function names, you can give it the JavaScript function name scopeSelector
:
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
A scope descriptor is an Object that wraps an Array
of String
s. The Array describes a path from the root of the syntax tree to a token including all scope names for the entire path.
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
Config::get
accepts a scopeDescriptor
. You can get the value for your setting scoped to JavaScript function names via:
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
Editor::getRootScopeDescriptor
to get the language's descriptor. For example: [".source.js"]
Editor::scopeDescriptorForBufferPosition
to get the descriptor at a specific position in the buffer.Cursor::getScopeDescriptor
to get a cursor's descriptor based on position. eg. if the cursor were in the name of the method in our example it would return ["source.js", "meta.function.js", "entity.name.function.js"]
Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer—the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Atom to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Atom will only attempt to call deserialize if the two versions match, and otherwise return undefined. We plan on implementing a migration system in the future, but this at least protects you from improperly deserializing old state.
You should now have a better understanding of some of the core Atom APIs and systems.
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
The collection of Frequently Asked Questions about Atom.
Yes, Atom is licensed under the MIT license.
Since the 6th of May, 2014, Atom has been available for download free of charge for everyone. This includes business and enterprise use.
Prebuilt versions of Atom are available for OS X 10.10 or later, Windows 7 or later, RedHat Linux, and Ubuntu Linux.
If you would like to build from source on Windows, Linux, or OS X, see the Atom README for more information.
You can contribute by creating a package that adds something awesome to Atom!
Also, if you’d like to contribute to the core editor, one of the bundled packages, or one of the libraries that power Atom, just go to github.com/atom.
You should also read the contributing guide before getting started.
In the same way that aggregate usage information is important when developing a web application, we've found that it's just as important for desktop applications. By knowing which Atom features are being used the most, and how the editor is performing, we can focus our development efforts in the right place. For details on what data Atom is sending or to learn how to disable metrics gathering, visit https://github.com/atom/metrics.
The Atom team has no plans to make a cloud- or server-based version of Atom. For discussion of the idea, see the Atom message board.
The term "IDE" comes from Integrated Development Environment. It is intended as a set of tools that all work together: text editor, compiler, build or make integration, debugging, etc. Virtually all IDEs are tied specifically to a language or framework or tightly collected set of languages or frameworks. Some examples: Visual Studio for .NET and other Microsoft languages, RubyMine for Ruby, IntelliJ for Java, XCode for Apple technologies.
An editor is simply that, a tool that is designed to edit text. Typically they are optimized for programming languages though many programmer's text editors are branching out and adding features for non-programming text like Markdown or Org Mode. The key here is that text editors are designed to work with whatever language or framework you choose.
The tradeoff here is that while you can generally get off the ground faster if you're working within the realm of a given IDE, over the long term you spend a bunch of time retraining yourself when you inevitably change from one language or toolchain to the next. If you use an editor, you can continue to use the same workflows that you always have. Tools that you've built into your editor can be carried over to the next language and framework. Your editor becomes more powerful and more customized to how you want to work not just over years but potentially decades. Just ask people who use vim or Emacs ... both of which have been available for over 25 years!
So, if you want something that you can just jump into and be productive right away in a specific technology, perhaps an IDE is what you're looking for. If you want a tool that you can shape and customize into exactly what you want out of it even if it costs you some time up front configuring things, then an editor is probably more your speed ⚡
If you take a screenshot and blow it up you'll see something like this:
The text of lines 34-36 are subpixel antialiased. Line 37 is not. You can tell it is subpixel antialiased because one side of the characters will be shifted red and the other side will be shifted blue. The direction of the shift is dependent on the monitor being used.
You can find more information on subpixel rendering on Wikipedia.
Atom ships with the whitespace
package, which by default strips trailing whitespace from lines in your file, and inserts a final trailing newline to indicate end-of-file as per the POSIX standard.
You can disable this feature by going to the Packages list in the Settings View and finding the whitespace package:
Take a look at the Whitespace section for more information.
Atom's Safe Mode, which can be activated by completely exiting all instances of Atom and launching it again using the command atom --safe
from the command line, does the following:
~/.atom/packages
or ~/.atom/dev/packages
init.coffee
The intent of Safe Mode is to determine if a problem is being caused by a community package or is caused by built-in functionality of Atom. Disabling the init script was added because people tend to use the init script as a mini-package of sorts by adding code, commands and other functionality that would normally be in a package.
For more information on Safe Mode, check the debugging section.
The best place to get a question answered quickly is probably the Issues list for that specific package. You can find the Issues list for a package by going to that package's page on https://atom.io and clicking the Bugs button:
And you can always ask Atom-related questions in the official Atom message board. Someone here may know the answer! It's just with over 3,500 packages (as of early February 2016), the forum members may not know all answers for all packages 😀
As of Atom v1.12, a fix is available for this. See the blog post "The Wonderful World of Keyboards" for more information.
Juno is a development environment built on top of Atom but has enough separate customizations that they have their own message board. You will probably have better luck asking your question there.
This means that there is a proxy between you and our servers where someone (typically your employer) has installed a "self-signed" security certificate in the proxy. A self-signed certificate is one that isn't trusted by anyone but the person who created the certificate. Most security certificates are backed by known, trusted and certified companies. So Atom is warning you that your connection to our servers can be snooped and even hacked by whoever created the self-signed certificate. Since it is self-signed, Atom has no way of knowing who that is.
If you decide that unsecured connections to our servers is acceptable to you, you can use the following instructions.
DANGER
🚨 Danger: If you decide that unsecured connections to our servers is acceptable to you, you can use the following command:
apm config set strict-ssl false
+
PlatformIO is a development environment built on top of Atom but has enough separate customizations that they have their own message board. If your question has to do with PlatformIO specifically, you may have better luck getting your answer there.
Atom includes a feature called "custom file types" which you can use by adding some entries into your config.cson
that look like this:
core:
+ customFileTypes:
+ 'source.ruby': [
+ 'Cheffile'
+ 'this-is-also-ruby'
+ ]
+ 'source.cpp': [
+ 'h'
+ ]
+
The key (for example source.ruby
in the above snippet) is the language's scope name. The value is an array of file extensions, without the period, to match to that scope name.
You can make the Welcome screen stop showing up by unchecking this box in the welcome screen itself:
There are a couple different approaches, for example:
Other packages may be available now, you can search for Atom packages on the packages site.
The script package doesn't support accepting input from the user in the scripts it runs. The option with the best chance of success is to run the script or program from the terminal that comes with your operating system. If that isn't something you want to do, you could try one of the many terminal packages that are available.
See rgbkrk/atom-script#743 for details.
Atom shows there is a new version available but the version fails to install. You might have an error message showing a permissions error for example:
or it will say downloading but forever loops without restarting or updating.
You need to fix one or more of the following directories:
/Applications/Atom.app/
~/Library/Caches/com.github.atom.ShipIt
~/Library/Application Support/com.github.atom.ShipIt
Do the following:
whoami
And then execute these steps for each directory listed above in order:
stat -f "%Su" [directory]
root
root
then execute: sudo chown -R $(whoami) [directory]
Once you've done the above for both directories, start Atom normally and attempt to update 👍
The best way to tweak the syntax is to wrap your syntax style rules with atom-text-editor
and then prepend every scope with syntax--
. If you want your comments to be blue, for example, you would do the following:
atom-text-editor {
+ .syntax--comment {
+ color: blue;
+ }
+}
+
Atom doesn't have built-in support for building any type of code nor does it have built-in support for executing any kind of code other than JavaScript. Atom has a JavaScript interactive command-line (also known as a REPL) available through the Developer Tools. You can access the JavaScript REPL by using the following steps:
View > Developer > Toggle Developer Tools
If you're looking for a JavaScript execution environment beyond a REPL, Atom doesn't come with anything built-in for that purpose.
If you want to build code or execute scripts from within Atom there are a number of packages available including:
Resources on getting started with languages that are commonly asked about:
To uninstall Atom on macOS, run the following commands from the command line:
rm -rf ~/.atom
+rm -rf /usr/local/bin/atom
+rm -rf /usr/local/bin/apm
+rm -rf /Applications/Atom.app
+rm -rf ~/Library/Preferences/com.github.atom.plist
+rm -rf "~/Library/Application Support/com.github.atom.ShipIt"
+rm -rf "~/Library/Application Support/Atom"
+rm -rf "~/Library/Saved Application State/com.github.atom.savedState"
+rm -rf ~/Library/Caches/com.github.atom
+rm -rf ~/Library/Caches/Atom
+
In macOS Mojave v10.14.x, Apple disabled subpixel antialiasing on all monitors by default. Previous to Mojave, subpixel antialiasing was disabled only on Retina displays or on all displays if the "LCD font smoothing" option was disabled in System Preferences. With this change in Mojave, some users have reported that their fonts in Atom appear "thinner" or "dimmer" than they did previously. It can look better or worse depending on your font and theme selections, but in all cases this is completely a side-effect of the change that Apple made to their font rendering and is outside Atom's and Electron's control.
If this change is something that you dislike, there are a couple workarounds that the community has identified.
defaults write -g CGFontRenderingFontSmoothingDisabled -bool NO
This appears to re-enable the old "LCD font smoothing" option that was removed in Mojave. It is important to note that this is an OS-wide change.
Add the following to your stylesheet:
atom-text-editor {
+ font-weight: bold;
+}
+
This has the benefit of being a change local to Atom only if the rest of the OS looks fine to you.
With macOS 10.14 Mojave, Apple introduced new privacy protections similar to the existing protections found in iOS. Whenever an application attempts to access the files inside certain newly-protected directories, macOS asks the user whether they want to allow the application to access the content in those directories. These new privacy protections apply to the directories that contain your calendars, contacts, photos, mail, messages, and Time Machine backups.
Applications trigger these new macOS prompts when attempting to access these directories in any way. Simply attempting to list the files in one of these directories is enough to trigger these prompts. These protections even apply to Apple's own applications. For example, if you open Terminal.app
and try to list the files in ~/Library/Calendars
, macOS shows a prompt saying, '"Terminal" would like access to your calendar.'
Atom doesn't need access to these items, but you might unintentionally cause Atom to try to access these items. This commonly occurs when you open your home directory (~
) inside Atom and run a command that examines all files and directories beneath your home directory. For example, when you open the fuzzy-finder, it indexes the currently-open directory so that it can show you the available files:
Similarly, using find-and-replace across the entire home directory will cause Atom to scan all files under your home directory.
In addition to containing the files you're intending to edit inside Atom, your home directory also contains your files that have new OS-level protections in Mojave:
~/Library/Calendars
)~/Library/Application\ Support/AddressBook
~/Library/Mail
)~/Pictures/Photos\ Library.photoslibrary
)Before letting Atom read these files, Mojave is understandably asking whether you want Atom to be able to access this personal data.
Most people don't use Atom to view or edit their calendar files, contact files, photo library, etc. If you don't intend to use Atom to view/edit these files, then Atom doesn't need access to them. If you see a prompt from macOS saying that Atom would like to access these items, simply click Don't Allow.
To Atom, these items are just files on disk. Atom treats them exactly like any other file you would view in Atom. Therefore, if you allow Atom to access these items, you'll be able to use Atom to browse the directories that contain these items, and you'll be able to view the files in those directories. That's it. Nothing more.
Fortunately, macOS will only prompt you once for each type of personal data. In other words, you might see a prompt asking you whether Atom can access your calendar, and you might see a prompt asking you whether Atom can access your contacts, but once you make those decisions, you won't see those prompts again.
At any time, you can change your choices via System Preferences. Inside System Preferences, go to Security and Privacy
, click the Privacy
tab, and then click on Calendars
to manage which apps can access your Calendars
. The same goes for Contacts
, Photos
, etc.:
Many people understandably expect their text editor to be able to open any file on disk. And that's exactly how things worked prior to macOS Mojave. If you would like to restore that behavior, you can proactively instruct macOS to allow you to access all files with Atom. To do so:
Applications
folder in the FinderSecurity and Privacy
icon, click the Privacy
tab, and then click on Full Disk Access
in the left-hand sidebarFull Disk Access
as shown belowFor more details about soft wrap, see: https://flight-manual.atom.io/getting-started/sections/atom-basics/#soft-wrap.
If you're running Windows or Linux and you don't see the menu bar, it may have been accidentally toggled it off. You can bring it back from the Command Palette with Window: Toggle Menu Bar
or by pressing Alt.
You can disable hiding the menu bar with Alt by unchecking Settings > Core > Auto Hide Menu Bar
.
To use a newline in the result of find and replace, enable the Use Regex
option and use "\n" in your replacement text. For example, given this text:
hello, world, goodbye
+
If you'd like to replace the ", " with a newline so you end up with this text:
hello
+world
+goodbye
+
In the find and replace settings, enable Use Regex
, enter ", " as the find text, and enter "\n" as the replace text:
Then click Find All
and finally, click Replace All
.
That's the wrap guide. It is a visual indicator of when your lines of code are getting too long. It defaults to the column that your Preferred Line Length is set to.
If you want to turn it off, you can disable the wrap-guide package in the Settings View.
The Atom team has no plans to make a cloud- or server-based version of Atom. For discussion of the idea, see the Atom message board.
You can contribute by creating a package that adds something awesome to Atom!
Also, if you’d like to contribute to the core editor, one of the bundled packages, or one of the libraries that power Atom, just go to github.com/atom.
You should also read the contributing guide before getting started.
If you take a screenshot and blow it up you'll see something like this:
The text of lines 34-36 are subpixel antialiased. Line 37 is not. You can tell it is subpixel antialiased because one side of the characters will be shifted red and the other side will be shifted blue. The direction of the shift is dependent on the monitor being used.
You can find more information on subpixel rendering on Wikipedia.
The script package doesn't support accepting input from the user in the scripts it runs. The option with the best chance of success is to run the script or program from the terminal that comes with your operating system. If that isn't something you want to do, you could try one of the many terminal packages that are available.
See rgbkrk/atom-script#743 for details.
Atom doesn't have built-in support for building any type of code nor does it have built-in support for executing any kind of code other than JavaScript. Atom has a JavaScript interactive command-line (also known as a REPL) available through the Developer Tools. You can access the JavaScript REPL by using the following steps:
View > Developer > Toggle Developer Tools
If you're looking for a JavaScript execution environment beyond a REPL, Atom doesn't come with anything built-in for that purpose.
If you want to build code or execute scripts from within Atom there are a number of packages available including:
Resources on getting started with languages that are commonly asked about:
Atom includes a feature called "custom file types" which you can use by adding some entries into your config.cson
that look like this:
core:
+ customFileTypes:
+ 'source.ruby': [
+ 'Cheffile'
+ 'this-is-also-ruby'
+ ]
+ 'source.cpp': [
+ 'h'
+ ]
+
The key (for example source.ruby
in the above snippet) is the language's scope name. The value is an array of file extensions, without the period, to match to that scope name.
You can make the Welcome screen stop showing up by unchecking this box in the welcome screen itself:
There are a couple different approaches, for example:
Other packages may be available now, you can search for Atom packages on the packages site.
For more details about soft wrap, see: https://flight-manual.atom.io/getting-started/sections/atom-basics/#soft-wrap.
To uninstall Atom on macOS, run the following commands from the command line:
rm -rf ~/.atom
+rm -rf /usr/local/bin/atom
+rm -rf /usr/local/bin/apm
+rm -rf /Applications/Atom.app
+rm -rf ~/Library/Preferences/com.github.atom.plist
+rm -rf "~/Library/Application Support/com.github.atom.ShipIt"
+rm -rf "~/Library/Application Support/Atom"
+rm -rf "~/Library/Saved Application State/com.github.atom.savedState"
+rm -rf ~/Library/Caches/com.github.atom
+rm -rf ~/Library/Caches/Atom
+
To use a newline in the result of find and replace, enable the Use Regex
option and use "\n" in your replacement text. For example, given this text:
hello, world, goodbye
+
If you'd like to replace the ", " with a newline so you end up with this text:
hello
+world
+goodbye
+
In the find and replace settings, enable Use Regex
, enter ", " as the find text, and enter "\n" as the replace text:
Then click Find All
and finally, click Replace All
.
Atom shows there is a new version available but the version fails to install. You might have an error message showing a permissions error for example:
or it will say downloading but forever loops without restarting or updating.
You need to fix one or more of the following directories:
/Applications/Atom.app/
~/Library/Caches/com.github.atom.ShipIt
~/Library/Application Support/com.github.atom.ShipIt
Do the following:
whoami
And then execute these steps for each directory listed above in order:
stat -f "%Su" [directory]
root
root
then execute: sudo chown -R $(whoami) [directory]
Once you've done the above for both directories, start Atom normally and attempt to update 👍
The best place to get a question answered quickly is probably the Issues list for that specific package. You can find the Issues list for a package by going to that package's page on https://atom.io and clicking the Bugs button:
And you can always ask Atom-related questions in the official Atom message board. Someone here may know the answer! It's just with over 3,500 packages (as of early February 2016), the forum members may not know all answers for all packages 😀
This means that there is a proxy between you and our servers where someone (typically your employer) has installed a "self-signed" security certificate in the proxy. A self-signed certificate is one that isn't trusted by anyone but the person who created the certificate. Most security certificates are backed by known, trusted and certified companies. So Atom is warning you that your connection to our servers can be snooped and even hacked by whoever created the self-signed certificate. Since it is self-signed, Atom has no way of knowing who that is.
If you decide that unsecured connections to our servers is acceptable to you, you can use the following instructions.
DANGER
🚨 Danger: If you decide that unsecured connections to our servers is acceptable to you, you can use the following command:
apm config set strict-ssl false
+
Juno is a development environment built on top of Atom but has enough separate customizations that they have their own message board. You will probably have better luck asking your question there.
PlatformIO is a development environment built on top of Atom but has enough separate customizations that they have their own message board. If your question has to do with PlatformIO specifically, you may have better luck getting your answer there.
The best way to tweak the syntax is to wrap your syntax style rules with atom-text-editor
and then prepend every scope with syntax--
. If you want your comments to be blue, for example, you would do the following:
atom-text-editor {
+ .syntax--comment {
+ color: blue;
+ }
+}
+
As of Atom v1.12, a fix is available for this. See the blog post "The Wonderful World of Keyboards" for more information.
In macOS Mojave v10.14.x, Apple disabled subpixel antialiasing on all monitors by default. Previous to Mojave, subpixel antialiasing was disabled only on Retina displays or on all displays if the "LCD font smoothing" option was disabled in System Preferences. With this change in Mojave, some users have reported that their fonts in Atom appear "thinner" or "dimmer" than they did previously. It can look better or worse depending on your font and theme selections, but in all cases this is completely a side-effect of the change that Apple made to their font rendering and is outside Atom's and Electron's control.
If this change is something that you dislike, there are a couple workarounds that the community has identified.
defaults write -g CGFontRenderingFontSmoothingDisabled -bool NO
This appears to re-enable the old "LCD font smoothing" option that was removed in Mojave. It is important to note that this is an OS-wide change.
Add the following to your stylesheet:
atom-text-editor {
+ font-weight: bold;
+}
+
This has the benefit of being a change local to Atom only if the rest of the OS looks fine to you.
If you're running Windows or Linux and you don't see the menu bar, it may have been accidentally toggled it off. You can bring it back from the Command Palette with Window: Toggle Menu Bar
or by pressing Alt.
You can disable hiding the menu bar with Alt by unchecking Settings > Core > Auto Hide Menu Bar
.
Since the 6th of May, 2014, Atom has been available for download free of charge for everyone. This includes business and enterprise use.
Atom's Safe Mode, which can be activated by completely exiting all instances of Atom and launching it again using the command atom --safe
from the command line, does the following:
~/.atom/packages
or ~/.atom/dev/packages
init.coffee
The intent of Safe Mode is to determine if a problem is being caused by a community package or is caused by built-in functionality of Atom. Disabling the init script was added because people tend to use the init script as a mini-package of sorts by adding code, commands and other functionality that would normally be in a package.
For more information on Safe Mode, check the debugging section.
That's the wrap guide. It is a visual indicator of when your lines of code are getting too long. It defaults to the column that your Preferred Line Length is set to.
If you want to turn it off, you can disable the wrap-guide package in the Settings View.
Prebuilt versions of Atom are available for OS X 10.10 or later, Windows 7 or later, RedHat Linux, and Ubuntu Linux.
If you would like to build from source on Windows, Linux, or OS X, see the Atom README for more information.
The term "IDE" comes from Integrated Development Environment. It is intended as a set of tools that all work together: text editor, compiler, build or make integration, debugging, etc. Virtually all IDEs are tied specifically to a language or framework or tightly collected set of languages or frameworks. Some examples: Visual Studio for .NET and other Microsoft languages, RubyMine for Ruby, IntelliJ for Java, XCode for Apple technologies.
An editor is simply that, a tool that is designed to edit text. Typically they are optimized for programming languages though many programmer's text editors are branching out and adding features for non-programming text like Markdown or Org Mode. The key here is that text editors are designed to work with whatever language or framework you choose.
The tradeoff here is that while you can generally get off the ground faster if you're working within the realm of a given IDE, over the long term you spend a bunch of time retraining yourself when you inevitably change from one language or toolchain to the next. If you use an editor, you can continue to use the same workflows that you always have. Tools that you've built into your editor can be carried over to the next language and framework. Your editor becomes more powerful and more customized to how you want to work not just over years but potentially decades. Just ask people who use vim or Emacs ... both of which have been available for over 25 years!
So, if you want something that you can just jump into and be productive right away in a specific technology, perhaps an IDE is what you're looking for. If you want a tool that you can shape and customize into exactly what you want out of it even if it costs you some time up front configuring things, then an editor is probably more your speed ⚡
In the same way that aggregate usage information is important when developing a web application, we've found that it's just as important for desktop applications. By knowing which Atom features are being used the most, and how the editor is performing, we can focus our development efforts in the right place. For details on what data Atom is sending or to learn how to disable metrics gathering, visit https://github.com/atom/metrics.
With macOS 10.14 Mojave, Apple introduced new privacy protections similar to the existing protections found in iOS. Whenever an application attempts to access the files inside certain newly-protected directories, macOS asks the user whether they want to allow the application to access the content in those directories. These new privacy protections apply to the directories that contain your calendars, contacts, photos, mail, messages, and Time Machine backups.
Applications trigger these new macOS prompts when attempting to access these directories in any way. Simply attempting to list the files in one of these directories is enough to trigger these prompts. These protections even apply to Apple's own applications. For example, if you open Terminal.app
and try to list the files in ~/Library/Calendars
, macOS shows a prompt saying, '"Terminal" would like access to your calendar.'
Atom doesn't need access to these items, but you might unintentionally cause Atom to try to access these items. This commonly occurs when you open your home directory (~
) inside Atom and run a command that examines all files and directories beneath your home directory. For example, when you open the fuzzy-finder, it indexes the currently-open directory so that it can show you the available files:
Similarly, using find-and-replace across the entire home directory will cause Atom to scan all files under your home directory.
In addition to containing the files you're intending to edit inside Atom, your home directory also contains your files that have new OS-level protections in Mojave:
~/Library/Calendars
)~/Library/Application\ Support/AddressBook
~/Library/Mail
)~/Pictures/Photos\ Library.photoslibrary
)Before letting Atom read these files, Mojave is understandably asking whether you want Atom to be able to access this personal data.
Most people don't use Atom to view or edit their calendar files, contact files, photo library, etc. If you don't intend to use Atom to view/edit these files, then Atom doesn't need access to them. If you see a prompt from macOS saying that Atom would like to access these items, simply click Don't Allow.
To Atom, these items are just files on disk. Atom treats them exactly like any other file you would view in Atom. Therefore, if you allow Atom to access these items, you'll be able to use Atom to browse the directories that contain these items, and you'll be able to view the files in those directories. That's it. Nothing more.
Fortunately, macOS will only prompt you once for each type of personal data. In other words, you might see a prompt asking you whether Atom can access your calendar, and you might see a prompt asking you whether Atom can access your contacts, but once you make those decisions, you won't see those prompts again.
At any time, you can change your choices via System Preferences. Inside System Preferences, go to Security and Privacy
, click the Privacy
tab, and then click on Calendars
to manage which apps can access your Calendars
. The same goes for Contacts
, Photos
, etc.:
Many people understandably expect their text editor to be able to open any file on disk. And that's exactly how things worked prior to macOS Mojave. If you would like to restore that behavior, you can proactively instruct macOS to allow you to access all files with Atom. To do so:
Applications
folder in the FinderSecurity and Privacy
icon, click the Privacy
tab, and then click on Full Disk Access
in the left-hand sidebarFull Disk Access
as shown belowAtom ships with the whitespace
package, which by default strips trailing whitespace from lines in your file, and inserts a final trailing newline to indicate end-of-file as per the POSIX standard.
You can disable this feature by going to the Packages list in the Settings View and finding the whitespace package:
Take a look at the Whitespace section for more information.
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, you use this at your own risk. Current Pulsar documentation for this section is found at the documentation home.
This chapter is about getting started with Atom.
There are a lot of text editors out there; why should you spend your time learning about and using Atom?
Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Atom to build Atom, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Atom is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local subprocesses was a non-starter.
For this reason, we didn't build Atom as a traditional web application. Instead, Atom is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Atom window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Atom, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Atom on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Atom as a perfect complement to GitHub's primary mission of building better software by working together. Atom is a long-term investment, and GitHub will continue to support its development with a dedicated team going forward. But we also know that we can't achieve our vision for Atom alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
The entire Atom editor is free and open source and is available under the https://github.com/atom organization.
To get started with Atom, we'll need to get it on your system. This section will go over installing Atom on your system, as well as the basics of how to build it from source.
Installing Atom should be fairly simple. Generally, you can go to https://atom.io and you should see a download button as shown here:
The button or buttons should be specific to your platform and the download package should be easily installable. However, let's go over them here in a bit of detail.
You should consider updating Atom periodically for the latest improvements to the software. Additionally, When Atom receives hotfixes for security vulnerabilities you will want to update your version of Atom as soon as possible.
Atom stores configuration and state in a .atom
directory usually located in your home directory (%userprofile%
on Windows). You can however run Atom in portable mode where both the app and the configuration are stored together such as on a removable storage device.
To setup Atom in portable mode download the zip/tar.gz package for your system and extract it to your removable storage.
.atom
directory must be writeable.atom
directory to your portable device.atom
directory - just create a subdirectory called electronUserData
inside .atom
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)The Hacking on Atom Core section of the flight manual covers instructions on how to clone and build the source code if you prefer that option.
If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ apm config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure apm
to use it by running:
$ apm config set https-proxy <em>YOUR_PROXY_ADDRESS</em>
+
You can run apm config get https-proxy
to verify it has been set correctly.
Now that Atom is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Atom for the first time, you should get a screen that looks like this:
This is the Atom welcome screen and gives you a pretty good starting point for how to get started with the editor.
You can find definitions for all of the various terms that we use throughout the manual in our Glossary.
In that welcome screen, we are introduced to probably the most important command in Atom, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like Cmd+Shift+PCtrl+Shift+P to demonstrate how to run a command. These are the default keybindings for the platform that we detected you running.
If you want to see a different platform than the one we detected, you may choose a different one by using the platform selector near the top of the page:
If the Platform Selector is not present, then the current page doesn't have any platform-specific content.
If you have customized your Atom keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Atom. Instead of clicking around all the application menus to look for something, you can press Cmd+Shift+PCtrl+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut keystrokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Atom has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Atom Packages.
To open the Settings View, you can: ::: tabs Settings View
:::
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Atom. Atom ships with 4 different UI themes, dark and light variants of the Atom and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
There are also dozens of themes on https://atom.io that you can choose from if you want something different. We will cover customizing a theme in Style Tweaks and creating your own theme in Creating a Theme.
You can use the Settings View to specify your whitespace and wrapping preferences.
Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the Tab key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.
The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.
In Basic Customization we will see how to set different wrap preferences for different types of files (for example, if you want to wrap Markdown files but not other files).
Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Atom. You can do it by choosing File > Open from the menu bar or by pressing Cmd+O Ctrl+O to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
Another way to open a file in Atom is from the command line using the atom
command. The Atom menu bar has a command named "Install Shell Commands" which installs the atom
and apm
commands if Atom wasn't able to install them itself .The atom
and apm
commands are installed automatically as a part of Atom's installation process.
You can run the atom
command with one or more file paths to open up those files in Atom.
$ atom --help
+> Atom Editor v1.8.0
+
+> Usage: atom [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Atom window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off atom [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+content/getting-started/sections/atom-basics.md:84:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ atom content/getting-started/sections/atom-basics.md:84
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+content/getting-started/sections/atom-basics.md:150:722
+$ atom content/getting-started/sections/atom-basics.md:150:722
+
Editing a file is pretty straightforward. You can click around and scroll with your mouse and type to change the content. There is no special editing mode or key commands. If you prefer editors with modes or more complex key commands, you should take a look at the Atom package list. There are a lot of packages that emulate popular styles.
To save a file you can choose File > Save from the menu bar or Cmd+SCtrl+S to save the file. If you choose File > Save As or press Cmd+Shift+SCtrl+Shift+S then you can save the current content in your editor under a different file name. Finally, you can choose File > Save All or press Alt+Cmd+S to save all the open files in Atom.
Atom doesn't just work with single files though; you will most likely spend most of your time working on projects with multiple files. To open a directory, choose the menu item File > OpenFile > Open Folder and select a directory from the dialog. You can also add more than one directory to your current Atom window, by choosing File > Add Project Folder from the menu bar or pressing Cmd+Shift+OCtrl+Shift+A.
You can open any number of directories from the command line by passing their paths to the atom
command line tool. For example, you could run the command atom ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Atom with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
You can also hide and show it with Cmd+\Ctrl+\ or the tree-view: toggle
command from the Command Palette, and Ctrl+0 Alt+\ will focus it. When the Tree view has focus you can press A, M, or Delete to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in FinderWindows Exploreryour native filesystem or copying the file path to the clipboard.
Note
Atom Packages
Like many parts of Atom, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Atom by default. Packages that are bundled with Atom are referred to as Core packages. Ones that aren't bundled with Atom are referred to as Community packages.
You can find the source code to the Tree View on GitHub at https://github.com/atom/tree-view.
This is one of the interesting things about Atom. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.
Once you have a project open in Atom, you can easily find and open any file within that project.
If you press Cmd+TCtrl+T or Cmd+PCtrl+P, the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.
You can also search through only the files currently opened (rather than every file in your project) with Cmd+BCtrl+B. This searches through your "buffers" or open files. You can also limit this fuzzy search with Cmd+Shift+BCtrl+Shift+B, which searches only through the files which are new or have been modified since your last Git commit.
The fuzzy finder uses the core.ignoredNames
, fuzzy-finder.ignoredNames
and core.excludeVCSIgnoredPaths
configuration settings to filter out files and folders that will not be shown. If you have a project with tons of files you don't want it to search through, you can add patterns or paths to either of these config settings or your standard .gitignore
files. We'll learn more about config settings in Global Configuration Settings, but for now you can easily set these in the Settings View under Core Settings.
Both core.ignoredNames
and fuzzy-finder.ignoredNames
are interpreted as glob patterns as implemented by the minimatch Node module.
Tips
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
You should now have a basic understanding of what Atom is and what you want to do with it. You should also have it installed on your system and be able to use it for the most basic text editing operations.
Now you're ready to start digging into the fun stuff.
Now that Atom is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Atom for the first time, you should get a screen that looks like this:
This is the Atom welcome screen and gives you a pretty good starting point for how to get started with the editor.
You can find definitions for all of the various terms that we use throughout the manual in our Glossary.
In that welcome screen, we are introduced to probably the most important command in Atom, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like Cmd+Shift+PCtrl+Shift+P to demonstrate how to run a command. These are the default keybindings for the platform that we detected you running.
If you want to see a different platform than the one we detected, you may choose a different one by using the platform selector near the top of the page:
If the Platform Selector is not present, then the current page doesn't have any platform-specific content.
If you have customized your Atom keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Atom. Instead of clicking around all the application menus to look for something, you can press Cmd+Shift+PCtrl+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut keystrokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Atom has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Atom Packages.
To open the Settings View, you can: ::: tabs Settings View
:::
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Atom. Atom ships with 4 different UI themes, dark and light variants of the Atom and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
There are also dozens of themes on https://atom.io that you can choose from if you want something different. We will cover customizing a theme in Style Tweaks and creating your own theme in Creating a Theme.
You can use the Settings View to specify your whitespace and wrapping preferences.
Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the Tab key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.
The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.
In Basic Customization we will see how to set different wrap preferences for different types of files (for example, if you want to wrap Markdown files but not other files).
Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Atom. You can do it by choosing File > Open from the menu bar or by pressing Cmd+O Ctrl+O to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
Another way to open a file in Atom is from the command line using the atom
command. The Atom menu bar has a command named "Install Shell Commands" which installs the atom
and apm
commands if Atom wasn't able to install them itself .The atom
and apm
commands are installed automatically as a part of Atom's installation process.
You can run the atom
command with one or more file paths to open up those files in Atom.
$ atom --help
+> Atom Editor v1.8.0
+
+> Usage: atom [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Atom window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off atom [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+content/getting-started/sections/atom-basics.md:84:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ atom content/getting-started/sections/atom-basics.md:84
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+content/getting-started/sections/atom-basics.md:150:722
+$ atom content/getting-started/sections/atom-basics.md:150:722
+
Editing a file is pretty straightforward. You can click around and scroll with your mouse and type to change the content. There is no special editing mode or key commands. If you prefer editors with modes or more complex key commands, you should take a look at the Atom package list. There are a lot of packages that emulate popular styles.
To save a file you can choose File > Save from the menu bar or Cmd+SCtrl+S to save the file. If you choose File > Save As or press Cmd+Shift+SCtrl+Shift+S then you can save the current content in your editor under a different file name. Finally, you can choose File > Save All or press Alt+Cmd+S to save all the open files in Atom.
Atom doesn't just work with single files though; you will most likely spend most of your time working on projects with multiple files. To open a directory, choose the menu item File > OpenFile > Open Folder and select a directory from the dialog. You can also add more than one directory to your current Atom window, by choosing File > Add Project Folder from the menu bar or pressing Cmd+Shift+OCtrl+Shift+A.
You can open any number of directories from the command line by passing their paths to the atom
command line tool. For example, you could run the command atom ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Atom with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
You can also hide and show it with Cmd+\Ctrl+\ or the tree-view: toggle
command from the Command Palette, and Ctrl+0 Alt+\ will focus it. When the Tree view has focus you can press A, M, or Delete to add, move or delete files and folders. You can also right-click on a file or folder in the Tree view to see many of the various options, including all of these plus showing the file in FinderWindows Exploreryour native filesystem or copying the file path to the clipboard.
Note
Atom Packages
Like many parts of Atom, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Atom by default. Packages that are bundled with Atom are referred to as Core packages. Ones that aren't bundled with Atom are referred to as Community packages.
You can find the source code to the Tree View on GitHub at https://github.com/atom/tree-view.
This is one of the interesting things about Atom. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.
Once you have a project open in Atom, you can easily find and open any file within that project.
If you press Cmd+TCtrl+T or Cmd+PCtrl+P, the Fuzzy Finder will pop up. This will let you quickly search for any file in your project by typing parts of the path.
You can also search through only the files currently opened (rather than every file in your project) with Cmd+BCtrl+B. This searches through your "buffers" or open files. You can also limit this fuzzy search with Cmd+Shift+BCtrl+Shift+B, which searches only through the files which are new or have been modified since your last Git commit.
The fuzzy finder uses the core.ignoredNames
, fuzzy-finder.ignoredNames
and core.excludeVCSIgnoredPaths
configuration settings to filter out files and folders that will not be shown. If you have a project with tons of files you don't want it to search through, you can add patterns or paths to either of these config settings or your standard .gitignore
files. We'll learn more about config settings in Global Configuration Settings, but for now you can easily set these in the Settings View under Core Settings.
Both core.ignoredNames
and fuzzy-finder.ignoredNames
are interpreted as glob patterns as implemented by the minimatch Node module.
Tips
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
To get started with Atom, we'll need to get it on your system. This section will go over installing Atom on your system, as well as the basics of how to build it from source.
Installing Atom should be fairly simple. Generally, you can go to https://atom.io and you should see a download button as shown here:
The button or buttons should be specific to your platform and the download package should be easily installable. However, let's go over them here in a bit of detail.
You should consider updating Atom periodically for the latest improvements to the software. Additionally, When Atom receives hotfixes for security vulnerabilities you will want to update your version of Atom as soon as possible.
Atom stores configuration and state in a .atom
directory usually located in your home directory (%userprofile%
on Windows). You can however run Atom in portable mode where both the app and the configuration are stored together such as on a removable storage device.
To setup Atom in portable mode download the zip/tar.gz package for your system and extract it to your removable storage.
.atom
directory must be writeable.atom
directory to your portable device.atom
directory - just create a subdirectory called electronUserData
inside .atom
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)The Hacking on Atom Core section of the flight manual covers instructions on how to clone and build the source code if you prefer that option.
If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ apm config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure apm
to use it by running:
$ apm config set https-proxy <em>YOUR_PROXY_ADDRESS</em>
+
You can run apm config get https-proxy
to verify it has been set correctly.
You should now have a basic understanding of what Atom is and what you want to do with it. You should also have it installed on your system and be able to use it for the most basic text editing operations.
Now you're ready to start digging into the fun stuff.
There are a lot of text editors out there; why should you spend your time learning about and using Atom?
Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Atom to build Atom, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Atom is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local subprocesses was a non-starter.
For this reason, we didn't build Atom as a traditional web application. Instead, Atom is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Atom window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Atom, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Atom on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Atom as a perfect complement to GitHub's primary mission of building better software by working together. Atom is a long-term investment, and GitHub will continue to support its development with a dedicated team going forward. But we also know that we can't achieve our vision for Atom alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
The entire Atom editor is free and open source and is available under the https://github.com/atom organization.
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, you use this at your own risk. Current Pulsar documentation for this section is found at the documentation home.
Now it's time to come to the "Hackable" part of the Hackable Editor. As we've seen throughout the second section, a huge part of Atom is made up of bundled packages. If you wish to add some functionality to Atom, you have access to the same APIs and tools that the core features of Atom has. From the tree-view to the command-palette to find-and-replace functionality, even the most core features of Atom are implemented as packages.
In this chapter, we're going to learn how to extend the functionality of Atom through writing packages. This will be everything from new user interfaces to new language grammars to new themes. We'll learn this by writing a series of increasingly complex packages together, introducing you to new APIs and tools and techniques as we need them.
If you're looking for an example using a specific API or feature, you can skip to the end of the chapter where we've indexed all the examples that way.
To begin, there are a few things we'll assume you know, at least to some degree. Since all of Atom is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.
While much of Atom has been converted to JavaScript, a lot of older code has been left implemented in CoffeeScript because changing it would have been too risky. Additionally, Atom's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:
MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
We'll go over examples like this in a bit, but this is what the language looks like. Just about everything you can do with CoffeeScript in Atom is also doable in JavaScript. You can brush up on CoffeeScript at coffeescript.org.
Less is an even simpler transition from CSS. It adds a number of useful things like variables and functions to CSS. You can learn about Less at lesscss.org. Our usage of Less won't get too complex in this book however, so as long as you know basic CSS you should be fine.
When Atom finishes loading, it will evaluate init.coffee
in your ~/.atom
%USERPROFILE%\.atom
directory, giving you a chance to run CoffeeScript code to make customizations. Code in this file has full access to Atom's API. If customizations become extensive, consider creating a package, which we will cover in Package: Word Count.
You can open the init.coffee
file in an editor from the Atom > Init ScriptFile > Init ScriptEdit > Init Script menu. This file can also be named init.js
and contain JavaScript code.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.coffee
file to have Atom greet you with an audio beep every time it loads:
atom.beep()
+
Because init.coffee
provides access to Atom's API, you can use it to implement useful commands without creating a new package or extending an existing one. Here's a command which uses the Selection API and Clipboard API to construct a Markdown link from the selected text and the clipboard contents as the URL:
atom.commands.add 'atom-text-editor', 'markdown:paste-as-link', ->
+ return unless editor = atom.workspace.getActiveTextEditor()
+
+ selection = editor.getLastSelection()
+ clipboardText = atom.clipboard.read()
+
+ selection.insertText("[#{selection.getText()}](#{clipboardText})")
+
Now, reload Atom and use the Command Palette to execute the new command, "Markdown: Paste As Link", by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a keybinding for the command.
Let's get started by writing a very simple package and looking at some of the tools needed to develop one effectively. We'll start by writing a package that tells you how many words are in the current buffer and display it in a small modal window.
The simplest way to start a package is to use the built-in package generator that ships with Atom. As you might expect by now, this generator is itself a separate package implemented in package-generator.
You can run the generator by invoking the command palette and searching for "Generate Package". A dialog will appear asking you to name your new project. Name it your-name-word-count
. Atom will then create that directory and fill it out with a skeleton project and link it into your ~/.atom/packages
%USERPROFILE%\.atom\packages
directory so it's loaded when you launch your editor next time.
Note
Note: You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on atom.io (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the your-name-word-count
package name, you should be safe 😀
You can see that Atom has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+├─ grammars/
+├─ keymaps/
+├─ lib/
+├─ menus/
+├─ spec/
+├─ snippets/
+├─ styles/
+├─ index.js
+└─ package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
Similar to Node modules, Atom packages contain a package.json
file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.
In addition to some of the regular Node package.json
keys available, Atom package.json
files have their own additions.
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Atom will default to looking for an index.coffee
or index.js
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Atom has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Warning: Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Atom's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Atom. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
activate(state)
: This optional method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the serialize()
method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).initialize(state)
: (Available in Atom 1.14 and above) This optional method is similar to activate()
but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after your package's deserializers have been called), initialize()
is guaranteed to be called before everything. Use activate()
if you want to be sure that the workspace is ready; use initialize()
if you need to do some setup prior to your deserializers or view providers being invoked.serialize()
: This optional method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's activate
method so you can restore your view to where the user left off.deactivate()
: This optional method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.Style sheets for your package should be placed in the styles
directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or Less, but Less is recommended.
Ideally, you won't need much in the way of styling. Atom provides a standard set of components which define both the colors and UI elements for any package that fits into Atom seamlessly. You can view all of Atom's UI components by opening the styleguide: open the command palette Cmd+Shift+PCtrl+Shift+P and search for styleguide
, or type Cmd+Ctrl+Shift+GCtrl+Shift+G.
If you do need special styling, try to keep only structural styles in the package style sheets. If you must specify colors and sizing, these should be taken from the active theme's ui-variables.less.
An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Atom UI, this means the key combination will work anywhere in the application.
We'll cover more advanced keybinding stuff a bit later in Keymaps in Depth.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your plugin.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Atom text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole plugin.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Atom but optional. The toggle
method is one Atom is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Atom starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Atom workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple plugin. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the plugin so other plugins could subscribe to these events.
// Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Tip: Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling atom.workspace.getActiveTextEditor()
.
Next we get the number of words by calling getText()
on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = `There are ${count} words.`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
Note: To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Atom windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Atom being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press Alt+Cmd+ICtrl+Shift+I, or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Atom.
Under the hood, Jasmine v1.3 executes your tests, so you can assume that any DSL available there is also available to your package.
Once you've got your test suite written, you can run it by pressing Alt+Cmd+Ctrl+PAlt+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the atom --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
We've now generated, customized and tested our first plugin for Atom. Congratulations! Now let's go ahead and publish it so it's available to the world.
Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with ascii art. When you run our new command with the word "cool" selected, it will be replaced with:
o888
+ ooooooo ooooooo ooooooo 888
+ 888 888 888 888 888 888 888
+ 888 888 888 888 888 888
+ 88ooo888 88ooo88 88ooo88 o888o
+
+
This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.
The final package can be viewed at https://github.com/atom/ascii-art.
To begin, press Cmd+Shift+PCtrl+Shift+P to bring up the Command Palette. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter ascii-art
as the name of the package.
Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
As in Counting Words, we're using atom.workspace.getActiveTextEditor()
to get the object that represents the active text editor. If this convert()
method is called when not focused on a text editor, nothing will happen.
Next we insert a string into the current text editor with the insertText()
method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing Alt+Cmd+Ctrl+LCtrl+Shift+F5.
Now open the Command Palette and search for the "Ascii Art: Convert" command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Atom launch faster by allowing Atom to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the "Ascii Art: Convert" command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
Warning: The Atom keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
Now we need to convert the selected text to ASCII art. To do this we will use the figlet Node module from npm. Open package.json
and add the latest version of figlet to the dependencies:
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command "Update Package Dependencies: Update" from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run "Update Package Dependencies: Update" whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(`\n${art}\n`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
There are a couple of new things in this example we should look at quickly. The first is the editor.getSelectedText()
which, as you might guess, returns the text that is currently selected.
We then call the Figlet code to convert that into something else and replace the current selection with it with the editor.insertText()
call.
In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.
We saw in our Word Count package how we could show information in a modal panel. However, panels aren't the only way to extend Atom's UI—you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Atom will restore them the next time you open the project. This system is used by Atom's tree view, as well as by third party packages like Nuclide for its console, debugger, outline view, and diagnostics (linter results).
For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at https://github.com/atom/active-editor-info.
To begin, press Cmd+Shift+PCtrl+Shift+P to bring up the Command Palette. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter active-editor-info
as the name of the package.
Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Atom. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Atom will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Atom uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Atom for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Atom to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the "Active Editor Info: Toggle" command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = `
+ <h2>${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> ${item.softWrapped}</li>
+ <li><b>Tab Length:</b> ${item.getTabLength()}</li>
+ <li><b>Encoding:</b> ${item.getEncoding()}</li>
+ <li><b>Line Count:</b> ${item.getLineCount()}</li>
+ </ul>
+ `;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Atom how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Atom and toggle the view with the "Active Editor Info: Toggle" command. Then reload Atom again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
Atom's interface is rendered using HTML, and it's styled via Less which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.
Atom supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the Atom > PreferencesFile > PreferencesEdit > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
package.json
(as covered in Atom package.json
). This file is used to help distribute your theme to Atom users.package.json
must contain a theme
key with a value of ui
or syntax
for Atom to recognize and load it as a theme.Let's create your first theme.
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Syntax Theme" to generate a new theme package. Select "Generate Syntax Theme," and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Tip: Syntax themes should end with -syntax and UI themes should end with -ui.
Atom will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with Cmd+,Ctrl+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Atom by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R to see the changes you made reflected in your Atom window. Pretty neat!
Tip
Tip: You can avoid reloading to see changes you make by opening an Atom window in Dev Mode. To open a Dev Mode Atom window run atom --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
Note: It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Atom's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
atom --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
apm link --dev
to symlink your repository to ~/.atom/dev/packages
Tip
Tip: Because we used apm link --dev
in the above instructions, if you break anything you can always close Atom and launch Atom normally to force Atom to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
These default values will be used as a fallback in case a theme doesn't define its own variables.
In any of your package's .less
files, you can access the theme variables by importing the ui-variables
or syntax-variables
file from Atom.
Your package should generally only specify structural styling, and these should come from the style guide. Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!
Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
Reloading by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R after you make changes to your theme is less than ideal. Atom supports live updating of styles on Atom windows in Dev Mode.
To launch a Dev Mode window:
atom --dev
If you'd like to reload all the styles at any time, you can use the shortcut Alt+Cmd+Ctrl+LAlt+Ctrl+R.
Atom is based on the Chrome browser, and supports Chrome's Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the Alt+Cmd+ICtrl+Shift+I shortcut.
The dev tools allow you to inspect elements and take a look at their CSS properties.
Check out Google's extensive tutorial for a short introduction.
If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The Styleguide is a page that renders every component Atom supports.
To open the Styleguide, open the command palette with Cmd+Shift+PCtrl+Shift+P and search for "styleguide", or use the shortcut Cmd+Ctrl+Shift+GCtrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes un-usable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Atom in "normal" mode to fix the issue, it's advised to open two Atom windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Atom users, it's time to publish it. 🎉
Follow the steps on the Publishing page. The example used is for the Word Count package, but publishing a theme works exactly the same.
Atom's syntax highlighting and code folding system is powered by Tree-sitter. Tree-sitter parsers create and maintain full syntax trees representing your code.
This syntax tree gives Atom a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Atom are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Atom: a parser and a grammar file.
Tree-sitter generates parsers based on context-free grammars that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Atom.
They can also be developed and tested at the command line, separately from Atom. Tree-sitter has its own documentation page on how to create these parsers. The Tree-sitter GitHub organization also contains a lot of example parsers that you can learn from, each in its own repository.
Once you have created a parser, you need to publish it to the NPM registry to use it in Atom. To do this, make sure you have a name
and version
in your parser's package.json
:
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Atom package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+├── LICENSE
+├── README.md
+├── grammars
+│ └── mylanguage.cson
+└── package.json
+
The mylanguage.cson
file specifies how Atom should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
scopeName
- A unique, stable identifier for the language. Atom users will use this in configuration files if they want to specify custom configuration based on the language.name
- A human readable name for the language.parser
- The name of the parser node module that will be used for parsing. This string will be passed directly to require()
in order to load the parser.type
- This should have the value tree-sitter
to indicate to Atom that this is a Tree-sitter grammar and not a TextMate grammar.Next, the file should contain some fields that indicate to Atom when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Atom uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
Note that in this selector, we're using the immediate child combinator (>
). Arbitrary descendant selectors without this combinator (for example 'call_expression identifier'
, which would match any identifier
occurring anywhere within a call_expression
) are currently not supported.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
You can use the :nth-child
pseudo-class to select nodes based on their order within their parent. For example, this example selects identifier
nodes which are the fourth (zero-indexed) child of a singleton_method
node.
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
Finally, you can use double-quoted strings in the selectors to select anonymous tokens in the syntax tree, like (
and :
. See the Tree-sitter documentation for more information about named vs anonymous tokens.
scopes:
+ '''
+ "*",
+ "/",
+ "+",
+ "-"
+ ''': 'keyword.operator'
+
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:
+
+ # Apply the classes `syntax--builtin` and `syntax--variable` to all
+ # `identifier` nodes whose text is `require`.
+ 'identifier': {exact: 'require', scopes: 'builtin.variable'},
+
+ # Apply the classes `syntax--type` and `syntax--integer` to all
+ # `primitive_type` nodes whose text starts with `int` or `uint`.
+ 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},
+
+ # Apply the classes `syntax--builtin`, `syntax--class`, and
+ # `syntax--name` to `constant` nodes with the text `Array`,
+ # `Hash` and `String`. For all other `constant` nodes, just
+ # apply the classes `syntax--class` and `syntax--name`.
+ 'constant': [
+ {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},
+ 'class.name'
+ ]
+
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.If multiple selectors in the scopes
object match a node, the node's classes will be decided based on the most specific selector. Note that the exact
and match
rules do not affect specificity, so you may need to supply the same exact
or match
rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector `call_expression > identifier` is more
+ # specific than the selector `identifier`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
For example, in JavaScript, tagged template literals sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:
// HTML in a template literal
+const htmlContent = html`<div>Hello ${name}</div>`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All `comment` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # `if_statement` nodes are foldable if they contain an anonymous
+ # "then" token and either an `elif_clause` or `else_clause` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # `elif_clause` or `else_clause`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Atom's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or un-comment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Atom:
Atom's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Atom's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
Note
Note: This tutorial is a work in progress.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Atom uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
Grammar files are written in the CSON or JSON format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Tip: Grammar packages should start with language-.
The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the keymaps
, lib
, menus
, and styles
folders. Furthermore, in package.json
, remove the activationCommands
section. Now create a new folder called grammars
, and inside that a file called flight-manual.cson
. This is the main file that we will be working with - start by populating it with a boilerplate template. Now let's go over what each key means.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \bFlight Manual\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\bFlight Manual\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
match
is where your regex is contained, and name
is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in Section 12.4 of the TextMate Manual.
Tip
Tip: All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Note: Astute readers may have noticed that the \b
was changed to \\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\b(Flight) (Manual)\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the ::: note Note
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. ::: note Note
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Tip: Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\b(Flight) (Manual)\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
first-mate
. Not necessary to write a grammar, but a good technical reference for what Atom is doing behind the scenes.Atom bundles a command line utility called apm
which we first used back in Command Line to search for and install packages via the command line. The apm
command can also be used to publish Atom packages to the public registry and update them.
There are a few things you should double check before publishing:
package.json
file has name
, description
, and repository
fields.package.json
file has a version
field with a value of "0.0.0"
.package.json
file has an engines
field that contains an entry for Atom such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Before you publish a package it is a good idea to check ahead of time if a package with the same name has already been published to the atom.io package registry. You can do that by visiting https://atom.io/packages/your-package-name
to see if the package already exists. If it does, update your package's name to something that is available before proceeding.
Now let's review what the apm publish
command does:
version
field in the package.json
file and commits it.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ apm publish minor
+
If this is the first package you are publishing, the apm publish
command may prompt you for your GitHub username and password. If you have two-factor authentication enabled, use a personal access token in lieu of a password. This is required to publish and you only need to enter this information the first time you publish. The credentials are stored securely in your keychain once you login.
Your package is now published and available on atom.io. Head on over to https://atom.io/packages/your-package-name
to see your package's page.
With apm publish
, you can bump the version and publish by using
$ apm publish <em>version-type</em>
+
where version-type
can be major
, minor
and patch
.
The major
option to the publish command tells apm to increment the first number of the version before publishing so the published version will be 1.0.0
and the Git tag created will be v1.0.0
.
The minor
option to the publish command tells apm to increment the second number of the version before publishing so the published version will be 0.1.0
and the Git tag created will be v0.1.0
.
The patch
option to the publish command tells apm to increment the third number of the version before publishing so the published version will be 0.0.1
and the Git tag created will be v0.0.1
.
Use major
when you make a change that breaks backwards compatibility, like changing defaults or removing features. Use minor
when adding new functionality or options, but without breaking backwards compatibility. Use patch
when you've changed the implementation of existing features, but without changing the behaviour or options of your package. Check out semantic versioning to learn more about best practices for versioning your package releases.
You can also run apm help publish
to see all the available options and apm help
to see all the other available commands.
Atom comes bundled with the Octicons 4.4.0 icon set. Use them to add icons to your packages.
NOTE: Some older icons from version
2.1.2
are still kept for backwards compatibility.
In the Styleguide under the "Icons" section you'll find all the Octicons that are available.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
Although icons can make your UI visually appealing, when used without a text label, it can be hard to guess its meaning. In cases where space for a text label is insufficient, consider adding a tooltip that appears on hover. Or a more subtle title="label"
attribute would help as well.
Atom provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when submitting issues:
You might be running into an issue which was already fixed in a more recent version of Atom than the one you're using.
If you're using a released version, check which version of Atom you're using:
$ atom --version
+> Atom : 1.8.0
+> Electron: 0.36.8
+> Chrome : 47.0.2526.110
+> Node : 5.1.1
+
Then check for the latest Stable version.
If you're building Atom from source, pull down the latest version of master and re-build.
A large part of Atom's functionality comes from packages you can install. Atom will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Atom from the terminal in safe mode:
$ atom --safe
+
This starts Atom, but does not load packages from ~/.atom/packages
or ~/.atom/dev/packages
and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Atom normally still produces the error, then try figuring out which package is causing trouble. Start Atom normally again and open the Settings View with Cmd+,Ctrl+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Atom or reload Atom with Alt+Cmd+Ctrl+LCtrl+Shift+F5 after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Atom saves a number of things about your environment when you exit in order to restore Atom to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Atom from working properly. In these cases, you may want to clear the state that Atom has saved.
DANGER
:rotatinglight: Danger: Clearing the saved state permanently destroys any state that Atom has saved _across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ atom --clear-window-state
+
In some cases, you may want to reset Atom to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
Once that is complete, you can launch Atom as normal. Everything will be just as if you first installed Atom.
Tip
Tip: The command given above doesn't delete the old configuration, just puts it somewhere that Atom can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the ~/.atom-backup
%USERPROFILE%\.atom-backup
directory.
If you develop or contribute to Atom packages, there may be left-over packages linked to your ~/.atom/packages
or ~/.atom/dev/packages
directories. You can use the apm links
command to list all linked packages:
$ apm links
+> /Users/octocat/.atom/dev/packages (0)
+> └── (no links)
+> /Users/octocat/.atom/packages (1)
+> └── color-picker -> /Users/octocat/github/color-picker
+
You can remove links using the apm unlink
command:
$ apm unlink color-picker
+> Unlinking /Users/octocat/.atom/packages/color-picker ✓
+
See apm links --help
and apm unlink --help
for more information on these commands.
Tip
Tip: You can also use apm unlink --all
to easily unlink all packages and themes.
If you have packages installed that use native Node modules, when you upgrade to a new version of Atom, they might need to be rebuilt. Atom detects this and through the incompatible-packages package displays an indicator in the status bar when this happens.
If you see this indicator, click it and follow the instructions.
In some cases, unexpected behavior might be caused by settings in Atom or in one of the packages.
Open Atom's Settings View with Cmd+,Ctrl+,, the Atom > PreferencesFile > PreferencesEdit > Preferences menu option, or the "Settings View: Open" command from the Command Palette.
Check Atom's settings in the Settings View, there's a description of most configuration options in the Basic Customization section. For example, if you want Atom to hide the invisible symbols representing whitespace characters, disable the "Show Invisibles" option.
Some of these options are also available on a per-language basis which means that they may be different for specific languages, for example JavaScript or Python. To check the per-language settings, open the settings for the language package under the Packages tab in the Settings View, for example the language-javascript or language-python package.
Since Atom ships with a set of packages and you can also install additional packages yourself, check the list of packages and their settings. For instance, if you'd like to get rid of the vertical line in the middle of the editor, disable the Wrap Guide package. And if you don't like it when Atom strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the whitespace package's settings.
You might have defined some custom styles, keymaps or snippets in one of your configuration files. In some situations, these personal hacks might be causing the unexpected behavior you're observing so try clearing those files and restarting Atom.
If a command is not executing when you press a key combination or the wrong command is executing, there might be an issue with the keybinding for that combination. Atom ships with the Keybinding Resolver, a neat package which helps you understand what key Atom saw you press and the command that was triggered because of it.
Show the keybinding resolver with Cmd+.Ctrl+. or with "Keybinding Resolver: Show" from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
If multiple keybindings are matched, Atom determines which keybinding will be executed based on the specificity of the selectors and the order in which they were loaded. If the command you wanted to trigger is listed in the Keybinding Resolver, but wasn't the one that was executed, this is normally explained by one of two causes:
The key combination was not used in the context defined by the keybinding's selector
For example, you can't trigger the keybinding for the tree-view:add-file
command if the Tree View is not focused.
There is another keybinding that took precedence
This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.
Atom loads core Atom keybindings and package keybindings first, and user-defined keybindings last. Since user-defined keybindings are loaded last, you can use your keymap.cson
file to tweak the keybindings and sort out problems like these. See the Keymaps in Depth section for more information.
If you notice that a package's keybindings are taking precedence over core Atom keybindings, it might be a good idea to report the issue on that package's GitHub repository. You can contact atom maintainers on Atom's github discussions
You can determine which fonts are being used to render a specific piece of text by using the Developer Tools. To open the Developer Tools press Alt+Cmd+ICtrl+Shift+I. Once the Developer Tools are open, click the "Elements" tab. Use the standard tools for finding the element containing the text you want to check. Once you have selected the element, you can click the "Computed" tab in the styles pane and scroll to the bottom. The list of fonts being used will be shown there:
When an unexpected error occurs in Atom, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press Alt-Cmd-ICtrl-Shift-I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
Note: When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
If Atom is taking a long time to start, you can use the Timecop package to get insight into where Atom spends time while loading.
Timecop displays the following information:
If a specific package has high load or activation times, you might consider reporting an Issue to the maintainers. You can also disable the package to potentially improve future startup times.
If you're experiencing performance problems in a particular situation, your Issue reports will be more valuable if you include a saved profile from Chrome's CPU profiler that gives some insight into what is slow.
To run a profile, open the Developer Tools with Alt+Cmd+ICtrl+Shift+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
To learn more, check out the Chrome documentation on CPU profiling.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Atom:
$ atom --profile-startup .
+
This will automatically capture a CPU profile as Atom is loading and open the Developer Tools once Atom loads. From there:
You can then include the startup profile in any Issue you report.
If you are having issues installing a package using apm install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run apm install --check
to see if the Atom package manager can build native code on your machine.
Check out the pre-requisites in the build instructions for your platform for more details.
If you encounter flickering or other rendering issues, you can stop Atom from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ atom --disable-gpu
+
Chromium (and thus Atom) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Atom to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ atom --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Atom flags as they will not be executed after the Chromium flags e.g.:
$ atom --safe --enable-gpu-rasterization --force-gpu-rasterization
+
We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Atom?
Atom uses Jasmine as its spec framework. Any new functionality should have specs to guard against regressions.
Atom specs and package specs are added to their respective spec
directory. The example below creates a spec for Atom core.
Spec files must end with -spec
so add sample-spec.coffee
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
The best way to learn about expectations is to read the Jasmine documentation about them. Below is a simple example.
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Atom includes the following:
toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domThese are defined in spec/spec-helper.coffee.
Writing Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Atom. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
For a more detailed documentation on asynchronous tests please visit the Jasmine documentation.
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Atom core specs when working on Atom itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the Travis CI For Your Packages and AppVeyor CI For Your Packages posts for more details.
To run tests on the command line, run Atom with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
atom --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
WARNING
Warning: This API is available as of 1.2.0-beta0, and it is experimental and subject to change. Test runner authors should be prepared to test their code against future beta releases until it stabilizes.
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Atom will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Atom will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Atom's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.atom
).enablePersistence
A boolean indicating whether the Atom environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via atom --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finish running. This exit code will be returned when running your tests via the command line.
Beginning in Atom 1.23, packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Warning: Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Atom package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Atom which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Atom from deferring activation of your package — see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Now that we've told Atom that we want our package to handle URIs beginning with atom://my-package/
via our handleURI
method, we need to actually write this method. Atom passes two arguments to your URI handler method; the first one is the fully-parsed URI plus query string, parsed with Node's url.parse(uri, true)
. The second argument is the raw, string URI; this is normally not needed since the first argument gives you structured information about the URI.
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Atom handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings — you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Atom will not activate your package until it has a URI for it to handle — it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately — disabling deferred activation means Atom takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Atom on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger atom with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Atom provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Atom runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)"c:\my test"
/my\ test
\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\
and d:\
Creation and deletion operations may take a few milliseconds to complete. If you need to remove many files and folders consider RimRAF which has built-in retry logic for this.
CRLF
LF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
It's possible that you have themes or grammars from TextMate that you like and use and would like to convert to Atom. If so, you're in luck because there are tools to help with the conversion.
Converting a TextMate bundle will allow you to use its editor preferences, snippets, and colorization inside Atom.
Let's convert the TextMate bundle for the R programming language. You can find other existing TextMate bundles on GitHub.
You can convert the R bundle with the following command:
$ apm init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the apm link
command, your new package is ready to use. Launch Atom and open a .r
file in the editor to see it in action!
This section will go over how to convert a TextMate theme to an Atom theme.
TextMate themes use plist files while Atom themes use CSS or Less to style the UI and syntax in the editor.
The utility that converts the theme first parses the theme's plist file and then creates comparable CSS rules and properties that will style Atom similarly.
Download the theme you wish to convert, you can browse existing TextMate themes on the TextMate website.
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ apm init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Atom and opening the Settings View with the Atom > PreferencesFile > PreferencesEdit > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
If you're hitting a bug in Atom or just want to experiment with adding a feature to the core of the system, you'll want to run Atom in Dev Mode with access to a local copy of the Atom source.
Follow the GitHub Help instructions on how to fork a repo.
Once you've set up your fork of the atom/atom repository, you can clone it to your local machine:
$ git clone git@github.com:<em>your-username</em>/atom.git
+
From there, you can navigate into the directory where you've cloned the Atom source code and run the bootstrap script to install all the required dependencies:
Once you have a local copy of Atom cloned and bootstrapped, you can then run Atom in Development Mode. But first, if you cloned Atom to somewhere other than ~/github/atom
%USERPROFILE%\github\atom
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Atom. To run Atom in Dev Mode, use the --dev
parameter from the terminal:
$ atom --dev <em>path-to-open</em>
+
Note
Note: If the atom command does not respond in the terminal, then try atom-dev or atom-beta. The suffix depends upon the particular source code that was cloned.
There are a couple benefits of running Atom in Dev Mode:
ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Atom is run using the source code from your local atom/atom
repository. This means that you don't have to run script/build
script\build
every time you change code. Just restart Atom 👍~/.atom/dev/packages
%USERPROFILE%\.atom\dev\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.window:reload
) to see changes to those.In order to run Atom Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <em>path-to-your-local-atom-repo</em>
+$ atom --test spec
+
In order to build Atom from source, you need to have a number of other requirements and take additional steps.
script/build
OptionsIf you think you know which package is causing the issue you are reporting, feel free to open up the issue in that specific repository instead. When in doubt just open the issue on the atom/atom repository but be aware that it may get closed and reopened in the proper package's repository.
The first step is creating your own clone. For some packages, you may also need to install the requirements necessary for building Atom in order to run apm install
.
For example, if you want to make changes to the tree-view
package, fork the repo on your github account, then clone it:
$ git clone git@github.com:<em>your-username</em>/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ apm install
+> Installing modules ✓
+
Now you can link it to development mode so when you run an Atom window with atom --dev
, you will use your fork instead of the built in package:
$ apm link -d
+
Editing a package in Atom is a bit of a circular experience: you're using Atom to modify itself. What happens if you temporarily break something? You don't want the version of Atom you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the "Application: Open Dev" command. You can also run dev mode from the command line with atom --dev
.
To load your package in development mode, create a symlink to it in ~/.atom/dev/packages
. This occurs automatically when you clone the package with apm develop
. You can also run apm link --dev
and apm unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running apm update
after pulling any upstream changes.
Several of Atom's core packages are maintained in the packages
directory of the atom/atom repository. If you would like to use one of these packages as a starting point for your own package, please follow the steps below.
Tips
Tip: In most cases, we recommend generating a brand new package or a brand new theme as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Atom package.
For the sake of this guide, let's assume that you want to start with the current code in the one-light-ui package, make some customizations to it, and publish your new package under the name "one-light-ui-plus".
Download the current contents of the atom/atom repository as a zip file
Unzip the file to a temporary location (for example /tmp/atom
C:\TEMP\atom
)
Copy the contents of the desired package into a working directory for your fork
$ <span class='platform-mac platform-linux'>cp -R /tmp/atom/packages/one-light-ui ~/src/one-light-ui-plus</span><span class='platform-windows'>xcopy C:\TEMP\atom\packages\one-light-ui C:\src\one-light-ui-plus /E /H /K</span>
+
Create a local repository and commit the initial contents
$ cd ~/src/one-light-ui-plus
+$ git init
+$ git commit -am "Import core Atom package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
Create a public repository on github.com for your new package
Follow the instructions in the github.com UI to push your code to your new online repository
Follow the steps in the Publishing guide to publish your new package
The code in the original package will continue to evolve over time, either to fix bugs or to add new enhancements. You may want to incorporate some or all of those updates into your package. To do so, you can follow these steps for merging upstream changes into your package.
Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team consolidated many core Atom packages into the atom/atom repository. For example, the one-light-ui package was originally maintained in the atom/one-light-ui repository, but it is now maintained in the packages/one-light-ui
directory in the atom/atom repository.
If you forked one of the core packages before it was moved into the atom/atom repository, and you want to continue merging upstream changes into your fork, please follow the steps below.
For the sake of this guide, let's assume that you forked the atom/one-light-ui repository, renamed your fork to one-light-ui-plus
, and made some customizations.
Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
Add the atom/atom repository as a git remote:
$ git remote add upstream https://github.com/atom/atom.git
+
Tip
Tip: Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the atom/atom repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log upstream/master -- packages/one-light-ui
+8ac9919a0 Bump up border size (Hugh Baht, 17 minutes ago)
+3bf4d226e Remove obsolete build status link in one-light-ui README (Jason Rudolph, 3 days ago)
+3edf64ad0 Merge pull request #42 from atom/sm-select-list (simurai, 2 weeks ago)
+...
+
Look through the log and identify the commits that you want to merge into your fork.
For each commit that you want to bring into your fork, use git format-patch
in conjunction with git am
. For example, to merge commit 8ac9919a0
into your fork:
$ git format-patch -1 --stdout 8ac9919a0 | git am -p3
+
Repeat this step for each commit that you want to merge into your fork.
If you finished this chapter, you should be an Atom-hacking master. We've discussed how you should work with CoffeeScript, and how to put it to good use in creating packages. You should also be able to do this in your own created theme now.
Even when something goes wrong, you should be able to debug this easily. But also fewer things should go wrong, because you are capable of writing great specs for Atom.
In the next chapter, we’ll go into more of a deep dive on individual internal APIs and systems of Atom, even looking at some Atom source to see how things are really getting done.
If you think you know which package is causing the issue you are reporting, feel free to open up the issue in that specific repository instead. When in doubt just open the issue on the atom/atom repository but be aware that it may get closed and reopened in the proper package's repository.
The first step is creating your own clone. For some packages, you may also need to install the requirements necessary for building Atom in order to run apm install
.
For example, if you want to make changes to the tree-view
package, fork the repo on your github account, then clone it:
$ git clone git@github.com:<em>your-username</em>/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ apm install
+> Installing modules ✓
+
Now you can link it to development mode so when you run an Atom window with atom --dev
, you will use your fork instead of the built in package:
$ apm link -d
+
Editing a package in Atom is a bit of a circular experience: you're using Atom to modify itself. What happens if you temporarily break something? You don't want the version of Atom you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the "Application: Open Dev" command. You can also run dev mode from the command line with atom --dev
.
To load your package in development mode, create a symlink to it in ~/.atom/dev/packages
. This occurs automatically when you clone the package with apm develop
. You can also run apm link --dev
and apm unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running apm update
after pulling any upstream changes.
It's possible that you have themes or grammars from TextMate that you like and use and would like to convert to Atom. If so, you're in luck because there are tools to help with the conversion.
Converting a TextMate bundle will allow you to use its editor preferences, snippets, and colorization inside Atom.
Let's convert the TextMate bundle for the R programming language. You can find other existing TextMate bundles on GitHub.
You can convert the R bundle with the following command:
$ apm init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the apm link
command, your new package is ready to use. Launch Atom and open a .r
file in the editor to see it in action!
This section will go over how to convert a TextMate theme to an Atom theme.
TextMate themes use plist files while Atom themes use CSS or Less to style the UI and syntax in the editor.
The utility that converts the theme first parses the theme's plist file and then creates comparable CSS rules and properties that will style Atom similarly.
Download the theme you wish to convert, you can browse existing TextMate themes on the TextMate website.
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ apm init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Atom and opening the Settings View with the Atom > PreferencesFile > PreferencesEdit > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
Several of Atom's core packages are maintained in the packages
directory of the atom/atom repository. If you would like to use one of these packages as a starting point for your own package, please follow the steps below.
Tips
Tip: In most cases, we recommend generating a brand new package or a brand new theme as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Atom package.
For the sake of this guide, let's assume that you want to start with the current code in the one-light-ui package, make some customizations to it, and publish your new package under the name "one-light-ui-plus".
Download the current contents of the atom/atom repository as a zip file
Unzip the file to a temporary location (for example /tmp/atom
C:\TEMP\atom
)
Copy the contents of the desired package into a working directory for your fork
$ <span class='platform-mac platform-linux'>cp -R /tmp/atom/packages/one-light-ui ~/src/one-light-ui-plus</span><span class='platform-windows'>xcopy C:\TEMP\atom\packages\one-light-ui C:\src\one-light-ui-plus /E /H /K</span>
+
Create a local repository and commit the initial contents
$ cd ~/src/one-light-ui-plus
+$ git init
+$ git commit -am "Import core Atom package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
Create a public repository on github.com for your new package
Follow the instructions in the github.com UI to push your code to your new online repository
Follow the steps in the Publishing guide to publish your new package
The code in the original package will continue to evolve over time, either to fix bugs or to add new enhancements. You may want to incorporate some or all of those updates into your package. To do so, you can follow these steps for merging upstream changes into your package.
Atom's syntax highlighting and code folding system is powered by Tree-sitter. Tree-sitter parsers create and maintain full syntax trees representing your code.
This syntax tree gives Atom a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Atom are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Atom: a parser and a grammar file.
Tree-sitter generates parsers based on context-free grammars that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Atom.
They can also be developed and tested at the command line, separately from Atom. Tree-sitter has its own documentation page on how to create these parsers. The Tree-sitter GitHub organization also contains a lot of example parsers that you can learn from, each in its own repository.
Once you have created a parser, you need to publish it to the NPM registry to use it in Atom. To do this, make sure you have a name
and version
in your parser's package.json
:
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Atom package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+├── LICENSE
+├── README.md
+├── grammars
+│ └── mylanguage.cson
+└── package.json
+
The mylanguage.cson
file specifies how Atom should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
scopeName
- A unique, stable identifier for the language. Atom users will use this in configuration files if they want to specify custom configuration based on the language.name
- A human readable name for the language.parser
- The name of the parser node module that will be used for parsing. This string will be passed directly to require()
in order to load the parser.type
- This should have the value tree-sitter
to indicate to Atom that this is a Tree-sitter grammar and not a TextMate grammar.Next, the file should contain some fields that indicate to Atom when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Atom uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
Note that in this selector, we're using the immediate child combinator (>
). Arbitrary descendant selectors without this combinator (for example 'call_expression identifier'
, which would match any identifier
occurring anywhere within a call_expression
) are currently not supported.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
You can use the :nth-child
pseudo-class to select nodes based on their order within their parent. For example, this example selects identifier
nodes which are the fourth (zero-indexed) child of a singleton_method
node.
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
Finally, you can use double-quoted strings in the selectors to select anonymous tokens in the syntax tree, like (
and :
. See the Tree-sitter documentation for more information about named vs anonymous tokens.
scopes:
+ '''
+ "*",
+ "/",
+ "+",
+ "-"
+ ''': 'keyword.operator'
+
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:
+
+ # Apply the classes `syntax--builtin` and `syntax--variable` to all
+ # `identifier` nodes whose text is `require`.
+ 'identifier': {exact: 'require', scopes: 'builtin.variable'},
+
+ # Apply the classes `syntax--type` and `syntax--integer` to all
+ # `primitive_type` nodes whose text starts with `int` or `uint`.
+ 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},
+
+ # Apply the classes `syntax--builtin`, `syntax--class`, and
+ # `syntax--name` to `constant` nodes with the text `Array`,
+ # `Hash` and `String`. For all other `constant` nodes, just
+ # apply the classes `syntax--class` and `syntax--name`.
+ 'constant': [
+ {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},
+ 'class.name'
+ ]
+
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.If multiple selectors in the scopes
object match a node, the node's classes will be decided based on the most specific selector. Note that the exact
and match
rules do not affect specificity, so you may need to supply the same exact
or match
rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector `call_expression > identifier` is more
+ # specific than the selector `identifier`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
For example, in JavaScript, tagged template literals sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:
// HTML in a template literal
+const htmlContent = html`<div>Hello ${name}</div>`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All `comment` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # `if_statement` nodes are foldable if they contain an anonymous
+ # "then" token and either an `elif_clause` or `else_clause` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # `elif_clause` or `else_clause`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Atom's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or un-comment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Atom:
Atom's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Atom's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
Note
Note: This tutorial is a work in progress.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Atom uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
Grammar files are written in the CSON or JSON format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Tip: Grammar packages should start with language-.
The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the keymaps
, lib
, menus
, and styles
folders. Furthermore, in package.json
, remove the activationCommands
section. Now create a new folder called grammars
, and inside that a file called flight-manual.cson
. This is the main file that we will be working with - start by populating it with a boilerplate template. Now let's go over what each key means.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \bFlight Manual\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\bFlight Manual\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
match
is where your regex is contained, and name
is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in Section 12.4 of the TextMate Manual.
Tip
Tip: All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Note: Astute readers may have noticed that the \b
was changed to \\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\b(Flight) (Manual)\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the ::: note Note
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. ::: note Note
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Tip: Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\b(Flight) (Manual)\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
first-mate
. Not necessary to write a grammar, but a good technical reference for what Atom is doing behind the scenes.Atom's interface is rendered using HTML, and it's styled via Less which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.
Atom supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the Atom > PreferencesFile > PreferencesEdit > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
package.json
(as covered in Atom package.json
). This file is used to help distribute your theme to Atom users.package.json
must contain a theme
key with a value of ui
or syntax
for Atom to recognize and load it as a theme.Let's create your first theme.
To get started, press Cmd+Shift+PCtrl+Shift+P and start typing "Generate Syntax Theme" to generate a new theme package. Select "Generate Syntax Theme," and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Tip: Syntax themes should end with -syntax and UI themes should end with -ui.
Atom will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with Cmd+,Ctrl+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Atom by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R to see the changes you made reflected in your Atom window. Pretty neat!
Tip
Tip: You can avoid reloading to see changes you make by opening an Atom window in Dev Mode. To open a Dev Mode Atom window run atom --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
Note: It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Atom's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
atom --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
apm link --dev
to symlink your repository to ~/.atom/dev/packages
Tip
Tip: Because we used apm link --dev
in the above instructions, if you break anything you can always close Atom and launch Atom normally to force Atom to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
These default values will be used as a fallback in case a theme doesn't define its own variables.
In any of your package's .less
files, you can access the theme variables by importing the ui-variables
or syntax-variables
file from Atom.
Your package should generally only specify structural styling, and these should come from the style guide. Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!
Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
Reloading by pressing Alt+Cmd+Ctrl+LAlt+Ctrl+R after you make changes to your theme is less than ideal. Atom supports live updating of styles on Atom windows in Dev Mode.
To launch a Dev Mode window:
atom --dev
If you'd like to reload all the styles at any time, you can use the shortcut Alt+Cmd+Ctrl+LAlt+Ctrl+R.
Atom is based on the Chrome browser, and supports Chrome's Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the Alt+Cmd+ICtrl+Shift+I shortcut.
The dev tools allow you to inspect elements and take a look at their CSS properties.
Check out Google's extensive tutorial for a short introduction.
If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The Styleguide is a page that renders every component Atom supports.
To open the Styleguide, open the command palette with Cmd+Shift+PCtrl+Shift+P and search for "styleguide", or use the shortcut Cmd+Ctrl+Shift+GCtrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes un-usable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Atom in "normal" mode to fix the issue, it's advised to open two Atom windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Atom users, it's time to publish it. 🎉
Follow the steps on the Publishing page. The example used is for the Word Count package, but publishing a theme works exactly the same.
Atom runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)"c:\my test"
/my\ test
\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\
and d:\
Creation and deletion operations may take a few milliseconds to complete. If you need to remove many files and folders consider RimRAF which has built-in retry logic for this.
CRLF
LF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
Atom provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when submitting issues:
You might be running into an issue which was already fixed in a more recent version of Atom than the one you're using.
If you're using a released version, check which version of Atom you're using:
$ atom --version
+> Atom : 1.8.0
+> Electron: 0.36.8
+> Chrome : 47.0.2526.110
+> Node : 5.1.1
+
Then check for the latest Stable version.
If you're building Atom from source, pull down the latest version of master and re-build.
A large part of Atom's functionality comes from packages you can install. Atom will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Atom from the terminal in safe mode:
$ atom --safe
+
This starts Atom, but does not load packages from ~/.atom/packages
or ~/.atom/dev/packages
and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Atom normally still produces the error, then try figuring out which package is causing trouble. Start Atom normally again and open the Settings View with Cmd+,Ctrl+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Atom or reload Atom with Alt+Cmd+Ctrl+LCtrl+Shift+F5 after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Atom saves a number of things about your environment when you exit in order to restore Atom to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Atom from working properly. In these cases, you may want to clear the state that Atom has saved.
DANGER
:rotatinglight: Danger: Clearing the saved state permanently destroys any state that Atom has saved _across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ atom --clear-window-state
+
In some cases, you may want to reset Atom to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
Once that is complete, you can launch Atom as normal. Everything will be just as if you first installed Atom.
Tip
Tip: The command given above doesn't delete the old configuration, just puts it somewhere that Atom can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the ~/.atom-backup
%USERPROFILE%\.atom-backup
directory.
If you develop or contribute to Atom packages, there may be left-over packages linked to your ~/.atom/packages
or ~/.atom/dev/packages
directories. You can use the apm links
command to list all linked packages:
$ apm links
+> /Users/octocat/.atom/dev/packages (0)
+> └── (no links)
+> /Users/octocat/.atom/packages (1)
+> └── color-picker -> /Users/octocat/github/color-picker
+
You can remove links using the apm unlink
command:
$ apm unlink color-picker
+> Unlinking /Users/octocat/.atom/packages/color-picker ✓
+
See apm links --help
and apm unlink --help
for more information on these commands.
Tip
Tip: You can also use apm unlink --all
to easily unlink all packages and themes.
If you have packages installed that use native Node modules, when you upgrade to a new version of Atom, they might need to be rebuilt. Atom detects this and through the incompatible-packages package displays an indicator in the status bar when this happens.
If you see this indicator, click it and follow the instructions.
In some cases, unexpected behavior might be caused by settings in Atom or in one of the packages.
Open Atom's Settings View with Cmd+,Ctrl+,, the Atom > PreferencesFile > PreferencesEdit > Preferences menu option, or the "Settings View: Open" command from the Command Palette.
Check Atom's settings in the Settings View, there's a description of most configuration options in the Basic Customization section. For example, if you want Atom to hide the invisible symbols representing whitespace characters, disable the "Show Invisibles" option.
Some of these options are also available on a per-language basis which means that they may be different for specific languages, for example JavaScript or Python. To check the per-language settings, open the settings for the language package under the Packages tab in the Settings View, for example the language-javascript or language-python package.
Since Atom ships with a set of packages and you can also install additional packages yourself, check the list of packages and their settings. For instance, if you'd like to get rid of the vertical line in the middle of the editor, disable the Wrap Guide package. And if you don't like it when Atom strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the whitespace package's settings.
You might have defined some custom styles, keymaps or snippets in one of your configuration files. In some situations, these personal hacks might be causing the unexpected behavior you're observing so try clearing those files and restarting Atom.
If a command is not executing when you press a key combination or the wrong command is executing, there might be an issue with the keybinding for that combination. Atom ships with the Keybinding Resolver, a neat package which helps you understand what key Atom saw you press and the command that was triggered because of it.
Show the keybinding resolver with Cmd+.Ctrl+. or with "Keybinding Resolver: Show" from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
If multiple keybindings are matched, Atom determines which keybinding will be executed based on the specificity of the selectors and the order in which they were loaded. If the command you wanted to trigger is listed in the Keybinding Resolver, but wasn't the one that was executed, this is normally explained by one of two causes:
The key combination was not used in the context defined by the keybinding's selector
For example, you can't trigger the keybinding for the tree-view:add-file
command if the Tree View is not focused.
There is another keybinding that took precedence
This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.
Atom loads core Atom keybindings and package keybindings first, and user-defined keybindings last. Since user-defined keybindings are loaded last, you can use your keymap.cson
file to tweak the keybindings and sort out problems like these. See the Keymaps in Depth section for more information.
If you notice that a package's keybindings are taking precedence over core Atom keybindings, it might be a good idea to report the issue on that package's GitHub repository. You can contact atom maintainers on Atom's github discussions
You can determine which fonts are being used to render a specific piece of text by using the Developer Tools. To open the Developer Tools press Alt+Cmd+ICtrl+Shift+I. Once the Developer Tools are open, click the "Elements" tab. Use the standard tools for finding the element containing the text you want to check. Once you have selected the element, you can click the "Computed" tab in the styles pane and scroll to the bottom. The list of fonts being used will be shown there:
When an unexpected error occurs in Atom, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press Alt-Cmd-ICtrl-Shift-I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
Note: When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
If Atom is taking a long time to start, you can use the Timecop package to get insight into where Atom spends time while loading.
Timecop displays the following information:
If a specific package has high load or activation times, you might consider reporting an Issue to the maintainers. You can also disable the package to potentially improve future startup times.
If you're experiencing performance problems in a particular situation, your Issue reports will be more valuable if you include a saved profile from Chrome's CPU profiler that gives some insight into what is slow.
To run a profile, open the Developer Tools with Alt+Cmd+ICtrl+Shift+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
To learn more, check out the Chrome documentation on CPU profiling.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Atom:
$ atom --profile-startup .
+
This will automatically capture a CPU profile as Atom is loading and open the Developer Tools once Atom loads. From there:
You can then include the startup profile in any Issue you report.
If you are having issues installing a package using apm install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run apm install --check
to see if the Atom package manager can build native code on your machine.
Check out the pre-requisites in the build instructions for your platform for more details.
If you encounter flickering or other rendering issues, you can stop Atom from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ atom --disable-gpu
+
Chromium (and thus Atom) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Atom to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ atom --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Atom flags as they will not be executed after the Chromium flags e.g.:
$ atom --safe --enable-gpu-rasterization --force-gpu-rasterization
+
If you're hitting a bug in Atom or just want to experiment with adding a feature to the core of the system, you'll want to run Atom in Dev Mode with access to a local copy of the Atom source.
Follow the GitHub Help instructions on how to fork a repo.
Once you've set up your fork of the atom/atom repository, you can clone it to your local machine:
$ git clone git@github.com:<em>your-username</em>/atom.git
+
From there, you can navigate into the directory where you've cloned the Atom source code and run the bootstrap script to install all the required dependencies:
Once you have a local copy of Atom cloned and bootstrapped, you can then run Atom in Development Mode. But first, if you cloned Atom to somewhere other than ~/github/atom
%USERPROFILE%\github\atom
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Atom. To run Atom in Dev Mode, use the --dev
parameter from the terminal:
$ atom --dev <em>path-to-open</em>
+
Note
Note: If the atom command does not respond in the terminal, then try atom-dev or atom-beta. The suffix depends upon the particular source code that was cloned.
There are a couple benefits of running Atom in Dev Mode:
ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Atom is run using the source code from your local atom/atom
repository. This means that you don't have to run script/build
script\build
every time you change code. Just restart Atom 👍~/.atom/dev/packages
%USERPROFILE%\.atom\dev\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.window:reload
) to see changes to those.In order to run Atom Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <em>path-to-your-local-atom-repo</em>
+$ atom --test spec
+
In order to build Atom from source, you need to have a number of other requirements and take additional steps.
script/build
OptionsBeginning in Atom 1.23, packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Warning: Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Atom package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Atom which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Atom from deferring activation of your package — see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Now that we've told Atom that we want our package to handle URIs beginning with atom://my-package/
via our handleURI
method, we need to actually write this method. Atom passes two arguments to your URI handler method; the first one is the fully-parsed URI plus query string, parsed with Node's url.parse(uri, true)
. The second argument is the raw, string URI; this is normally not needed since the first argument gives you structured information about the URI.
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Atom handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings — you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Atom will not activate your package until it has a URI for it to handle — it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately — disabling deferred activation means Atom takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Atom on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger atom with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Atom provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Atom comes bundled with the Octicons 4.4.0 icon set. Use them to add icons to your packages.
NOTE: Some older icons from version
2.1.2
are still kept for backwards compatibility.
In the Styleguide under the "Icons" section you'll find all the Octicons that are available.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
Although icons can make your UI visually appealing, when used without a text label, it can be hard to guess its meaning. In cases where space for a text label is insufficient, consider adding a tooltip that appears on hover. Or a more subtle title="label"
attribute would help as well.
Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team consolidated many core Atom packages into the atom/atom repository. For example, the one-light-ui package was originally maintained in the atom/one-light-ui repository, but it is now maintained in the packages/one-light-ui
directory in the atom/atom repository.
If you forked one of the core packages before it was moved into the atom/atom repository, and you want to continue merging upstream changes into your fork, please follow the steps below.
For the sake of this guide, let's assume that you forked the atom/one-light-ui repository, renamed your fork to one-light-ui-plus
, and made some customizations.
Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
Add the atom/atom repository as a git remote:
$ git remote add upstream https://github.com/atom/atom.git
+
Tip
Tip: Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the atom/atom repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log upstream/master -- packages/one-light-ui
+8ac9919a0 Bump up border size (Hugh Baht, 17 minutes ago)
+3bf4d226e Remove obsolete build status link in one-light-ui README (Jason Rudolph, 3 days ago)
+3edf64ad0 Merge pull request #42 from atom/sm-select-list (simurai, 2 weeks ago)
+...
+
Look through the log and identify the commits that you want to merge into your fork.
For each commit that you want to bring into your fork, use git format-patch
in conjunction with git am
. For example, to merge commit 8ac9919a0
into your fork:
$ git format-patch -1 --stdout 8ac9919a0 | git am -p3
+
Repeat this step for each commit that you want to merge into your fork.
We saw in our Word Count package how we could show information in a modal panel. However, panels aren't the only way to extend Atom's UI—you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Atom will restore them the next time you open the project. This system is used by Atom's tree view, as well as by third party packages like Nuclide for its console, debugger, outline view, and diagnostics (linter results).
For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at https://github.com/atom/active-editor-info.
To begin, press Cmd+Shift+PCtrl+Shift+P to bring up the Command Palette. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter active-editor-info
as the name of the package.
Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Atom. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Atom will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Atom uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Atom for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Atom to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the "Active Editor Info: Toggle" command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = `
+ <h2>${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> ${item.softWrapped}</li>
+ <li><b>Tab Length:</b> ${item.getTabLength()}</li>
+ <li><b>Encoding:</b> ${item.getEncoding()}</li>
+ <li><b>Line Count:</b> ${item.getLineCount()}</li>
+ </ul>
+ `;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Atom how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Atom and toggle the view with the "Active Editor Info: Toggle" command. Then reload Atom again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with ascii art. When you run our new command with the word "cool" selected, it will be replaced with:
o888
+ ooooooo ooooooo ooooooo 888
+ 888 888 888 888 888 888 888
+ 888 888 888 888 888 888
+ 88ooo888 88ooo88 88ooo88 o888o
+
+
This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.
The final package can be viewed at https://github.com/atom/ascii-art.
To begin, press Cmd+Shift+PCtrl+Shift+P to bring up the Command Palette. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter ascii-art
as the name of the package.
Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
As in Counting Words, we're using atom.workspace.getActiveTextEditor()
to get the object that represents the active text editor. If this convert()
method is called when not focused on a text editor, nothing will happen.
Next we insert a string into the current text editor with the insertText()
method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing Alt+Cmd+Ctrl+LCtrl+Shift+F5.
Now open the Command Palette and search for the "Ascii Art: Convert" command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Atom launch faster by allowing Atom to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the "Ascii Art: Convert" command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
Warning: The Atom keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
Now we need to convert the selected text to ASCII art. To do this we will use the figlet Node module from npm. Open package.json
and add the latest version of figlet to the dependencies:
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command "Update Package Dependencies: Update" from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run "Update Package Dependencies: Update" whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(`\n${art}\n`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
There are a couple of new things in this example we should look at quickly. The first is the editor.getSelectedText()
which, as you might guess, returns the text that is currently selected.
We then call the Figlet code to convert that into something else and replace the current selection with it with the editor.insertText()
call.
In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.
Let's get started by writing a very simple package and looking at some of the tools needed to develop one effectively. We'll start by writing a package that tells you how many words are in the current buffer and display it in a small modal window.
The simplest way to start a package is to use the built-in package generator that ships with Atom. As you might expect by now, this generator is itself a separate package implemented in package-generator.
You can run the generator by invoking the command palette and searching for "Generate Package". A dialog will appear asking you to name your new project. Name it your-name-word-count
. Atom will then create that directory and fill it out with a skeleton project and link it into your ~/.atom/packages
%USERPROFILE%\.atom\packages
directory so it's loaded when you launch your editor next time.
Note
Note: You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on atom.io (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the your-name-word-count
package name, you should be safe 😀
You can see that Atom has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+├─ grammars/
+├─ keymaps/
+├─ lib/
+├─ menus/
+├─ spec/
+├─ snippets/
+├─ styles/
+├─ index.js
+└─ package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
Similar to Node modules, Atom packages contain a package.json
file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.
In addition to some of the regular Node package.json
keys available, Atom package.json
files have their own additions.
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Atom will default to looking for an index.coffee
or index.js
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Atom has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/atom/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Warning: Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Atom's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Atom. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
activate(state)
: This optional method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the serialize()
method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).initialize(state)
: (Available in Atom 1.14 and above) This optional method is similar to activate()
but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after your package's deserializers have been called), initialize()
is guaranteed to be called before everything. Use activate()
if you want to be sure that the workspace is ready; use initialize()
if you need to do some setup prior to your deserializers or view providers being invoked.serialize()
: This optional method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's activate
method so you can restore your view to where the user left off.deactivate()
: This optional method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.Style sheets for your package should be placed in the styles
directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or Less, but Less is recommended.
Ideally, you won't need much in the way of styling. Atom provides a standard set of components which define both the colors and UI elements for any package that fits into Atom seamlessly. You can view all of Atom's UI components by opening the styleguide: open the command palette Cmd+Shift+PCtrl+Shift+P and search for styleguide
, or type Cmd+Ctrl+Shift+GCtrl+Shift+G.
If you do need special styling, try to keep only structural styles in the package style sheets. If you must specify colors and sizing, these should be taken from the active theme's ui-variables.less.
An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Atom UI, this means the key combination will work anywhere in the application.
We'll cover more advanced keybinding stuff a bit later in Keymaps in Depth.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your plugin.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Atom text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole plugin.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Atom but optional. The toggle
method is one Atom is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Atom starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Atom workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple plugin. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the plugin so other plugins could subscribe to these events.
// Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Tip: Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling atom.workspace.getActiveTextEditor()
.
Next we get the number of words by calling getText()
on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = `There are ${count} words.`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
Note: To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Atom windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Atom being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press Alt+Cmd+ICtrl+Shift+I, or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Atom.
Under the hood, Jasmine v1.3 executes your tests, so you can assume that any DSL available there is also available to your package.
Once you've got your test suite written, you can run it by pressing Alt+Cmd+Ctrl+PAlt+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the atom --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
We've now generated, customized and tested our first plugin for Atom. Congratulations! Now let's go ahead and publish it so it's available to the world.
Atom bundles a command line utility called apm
which we first used back in Command Line to search for and install packages via the command line. The apm
command can also be used to publish Atom packages to the public registry and update them.
There are a few things you should double check before publishing:
package.json
file has name
, description
, and repository
fields.package.json
file has a version
field with a value of "0.0.0"
.package.json
file has an engines
field that contains an entry for Atom such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Before you publish a package it is a good idea to check ahead of time if a package with the same name has already been published to the atom.io package registry. You can do that by visiting https://atom.io/packages/your-package-name
to see if the package already exists. If it does, update your package's name to something that is available before proceeding.
Now let's review what the apm publish
command does:
version
field in the package.json
file and commits it.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ apm publish minor
+
If this is the first package you are publishing, the apm publish
command may prompt you for your GitHub username and password. If you have two-factor authentication enabled, use a personal access token in lieu of a password. This is required to publish and you only need to enter this information the first time you publish. The credentials are stored securely in your keychain once you login.
Your package is now published and available on atom.io. Head on over to https://atom.io/packages/your-package-name
to see your package's page.
With apm publish
, you can bump the version and publish by using
$ apm publish <em>version-type</em>
+
where version-type
can be major
, minor
and patch
.
The major
option to the publish command tells apm to increment the first number of the version before publishing so the published version will be 1.0.0
and the Git tag created will be v1.0.0
.
The minor
option to the publish command tells apm to increment the second number of the version before publishing so the published version will be 0.1.0
and the Git tag created will be v0.1.0
.
The patch
option to the publish command tells apm to increment the third number of the version before publishing so the published version will be 0.0.1
and the Git tag created will be v0.0.1
.
Use major
when you make a change that breaks backwards compatibility, like changing defaults or removing features. Use minor
when adding new functionality or options, but without breaking backwards compatibility. Use patch
when you've changed the implementation of existing features, but without changing the behaviour or options of your package. Check out semantic versioning to learn more about best practices for versioning your package releases.
You can also run apm help publish
to see all the available options and apm help
to see all the other available commands.
If you finished this chapter, you should be an Atom-hacking master. We've discussed how you should work with CoffeeScript, and how to put it to good use in creating packages. You should also be able to do this in your own created theme now.
Even when something goes wrong, you should be able to debug this easily. But also fewer things should go wrong, because you are capable of writing great specs for Atom.
In the next chapter, we’ll go into more of a deep dive on individual internal APIs and systems of Atom, even looking at some Atom source to see how things are really getting done.
When Atom finishes loading, it will evaluate init.coffee
in your ~/.atom
%USERPROFILE%\.atom
directory, giving you a chance to run CoffeeScript code to make customizations. Code in this file has full access to Atom's API. If customizations become extensive, consider creating a package, which we will cover in Package: Word Count.
You can open the init.coffee
file in an editor from the Atom > Init ScriptFile > Init ScriptEdit > Init Script menu. This file can also be named init.js
and contain JavaScript code.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.coffee
file to have Atom greet you with an audio beep every time it loads:
atom.beep()
+
Because init.coffee
provides access to Atom's API, you can use it to implement useful commands without creating a new package or extending an existing one. Here's a command which uses the Selection API and Clipboard API to construct a Markdown link from the selected text and the clipboard contents as the URL:
atom.commands.add 'atom-text-editor', 'markdown:paste-as-link', ->
+ return unless editor = atom.workspace.getActiveTextEditor()
+
+ selection = editor.getLastSelection()
+ clipboardText = atom.clipboard.read()
+
+ selection.insertText("[#{selection.getText()}](#{clipboardText})")
+
Now, reload Atom and use the Command Palette to execute the new command, "Markdown: Paste As Link", by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a keybinding for the command.
To begin, there are a few things we'll assume you know, at least to some degree. Since all of Atom is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.
While much of Atom has been converted to JavaScript, a lot of older code has been left implemented in CoffeeScript because changing it would have been too risky. Additionally, Atom's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:
MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
We'll go over examples like this in a bit, but this is what the language looks like. Just about everything you can do with CoffeeScript in Atom is also doable in JavaScript. You can brush up on CoffeeScript at coffeescript.org.
Less is an even simpler transition from CSS. It adds a number of useful things like variables and functions to CSS. You can learn about Less at lesscss.org. Our usage of Less won't get too complex in this book however, so as long as you know basic CSS you should be fine.
We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Atom?
Atom uses Jasmine as its spec framework. Any new functionality should have specs to guard against regressions.
Atom specs and package specs are added to their respective spec
directory. The example below creates a spec for Atom core.
Spec files must end with -spec
so add sample-spec.coffee
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
The best way to learn about expectations is to read the Jasmine documentation about them. Below is a simple example.
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Atom includes the following:
toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domThese are defined in spec/spec-helper.coffee.
Writing Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Atom. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
For a more detailed documentation on asynchronous tests please visit the Jasmine documentation.
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Atom core specs when working on Atom itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the Travis CI For Your Packages and AppVeyor CI For Your Packages posts for more details.
To run tests on the command line, run Atom with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
atom --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
WARNING
Warning: This API is available as of 1.2.0-beta0, and it is experimental and subject to change. Test runner authors should be prepared to test their code against future beta releases until it stabilizes.
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Atom will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Atom will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Atom's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.atom
).enablePersistence
A boolean indicating whether the Atom environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via atom --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finish running. This exit code will be returned when running your tests via the command line.
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
This is an archive of the old Atom documentation as it appeared on their Flight Manual. Anything here is just a historical reference of our past, when Atom existed as a GitHub supported project.
This includes their original layout and formatting of how things were expected to appear. Anything listed here, may work, and it may not. This is taking into consideration their sunset and our picking up of their torch.
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
Collection of general resources for the rest of the manual.
Below is a list of some useful terms we use with regard to Atom.
A buffer is the text content of a file in Atom. It's basically the same as a file for most descriptions, but it's the version Atom has in memory. For instance, you can change the text of a buffer and it isn't written to its associated file until you save it.
A command is a bit of functionality in Atom that can be triggered by the user either through a keybinding or a menu item.
Docks are collapsible pane containers that attach to the left, right, and bottom sides of the Atom window.
Examples:
A key combination is some combination or sequence of keys that are pressed to perform a task.
Examples:
A key sequence is a special case of a key combination. It is a key combination that consists of keys that must be pressed and released in sequence. Ctrl+K Down is a key sequence. Alt+S is not a key sequence because it is two keys that are pressed and released together rather than in succession.
A keybinding is the mapping of a key combination, such as Ctrl+Enter to an Atom command.
A keymap is a collection of keybindings. It can also refer to a file or files containing keybindings for an Atom package or Atom itself.
An Atom plugin. There is a bunch more information in the section on Atom Packages.
A pane is a visual section of the editor space. Each pane can hold multiple pane items. There is always at least one pane in each Atom window.
A section of the Atom UI that can contain multiple panes.
Some item, often an editor, that is displayed within a pane. In the default configuration of Atom, pane items are represented by tabs at the top of each pane.
Note
Note: The reason why we don't call them "tabs" is because you can disable the tabs package and then there aren't any tabs. For a similar reason, we don't call them files because some things can be shown in a pane that aren't files, like the Settings View.
A piece of the Atom UI that is outside the editor space.
Examples:
Below is a list of some useful terms we use with regard to Atom.
A buffer is the text content of a file in Atom. It's basically the same as a file for most descriptions, but it's the version Atom has in memory. For instance, you can change the text of a buffer and it isn't written to its associated file until you save it.
A command is a bit of functionality in Atom that can be triggered by the user either through a keybinding or a menu item.
Docks are collapsible pane containers that attach to the left, right, and bottom sides of the Atom window.
Examples:
A key combination is some combination or sequence of keys that are pressed to perform a task.
Examples:
A key sequence is a special case of a key combination. It is a key combination that consists of keys that must be pressed and released in sequence. Ctrl+K Down is a key sequence. Alt+S is not a key sequence because it is two keys that are pressed and released together rather than in succession.
A keybinding is the mapping of a key combination, such as Ctrl+Enter to an Atom command.
A keymap is a collection of keybindings. It can also refer to a file or files containing keybindings for an Atom package or Atom itself.
An Atom plugin. There is a bunch more information in the section on Atom Packages.
A pane is a visual section of the editor space. Each pane can hold multiple pane items. There is always at least one pane in each Atom window.
A section of the Atom UI that can contain multiple panes.
Some item, often an editor, that is displayed within a pane. In the default configuration of Atom, pane items are represented by tabs at the top of each pane.
Note
Note: The reason why we don't call them "tabs" is because you can disable the tabs package and then there aren't any tabs. For a similar reason, we don't call them files because some things can be shown in a pane that aren't files, like the Settings View.
A piece of the Atom UI that is outside the editor space.
Examples:
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
In Atom 1.13
the Shadow DOM got removed from text editors. Find a guide how to migrate your theme or package in this appendix.
In Atom 1.13
the Shadow DOM got removed from text editors. For more background on the reasoning, check out the Pull Request where it was removed. In this guide you will learn how to migrate your theme or package.
Here is a quick summary to see all the changes at a glance:
Before | +/- | After |
---|---|---|
atom-text-editor::shadow {} | - ::shadow | atom-text-editor {} |
.class /deep/ .class {} | - /deep/ | .class .class {} |
atom-text-editor, :host {} | - :host | atom-text-editor {} |
.comment {} | + .syntax-- | .syntax--comment {} |
Below you'll find more detailed examples.
::shadow
Remove the ::shadow
pseudo-element selector from atom-text-editor
.
Before:
atom-text-editor::shadow .cursor {
+ border-color: hotpink;
+}
+
After:
atom-text-editor .cursor {
+ border-color: hotpink;
+}
+
/deep/
Remove the /deep/
combinator selector. It didn't get used that often, mainly to customize the scrollbars.
Before:
.scrollbars-visible-always /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
After:
.scrollbars-visible-always ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
:host
Remove the :host
pseudo-element selector. To scope any styles to the text editor, using atom-text-editor
is already enough.
Before:
atom-text-editor,
+:host {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
After:
atom-text-editor {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
syntax--
Add a syntax--
prefix to all grammar selectors used by language packages. It looks a bit verbose, but it will protect all the grammar selectors from accidental style pollution.
Before:
.comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .markup.link {
+ color: @mono-3;
+ }
+}
+
After:
.syntax--comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .syntax--markup.syntax--link {
+ color: @mono-3;
+ }
+}
+
Note: Selectors like the .gutter
, .indent-guide
, .cursor
among others, that are also part of Syntax themes, don't need a prefix. Only grammar selectors that get used by language packages. For example .syntax--keyword
, .syntax--keyword.syntax--operator.syntax--js
.
Atom also allowed you to target a specific shadow DOM context with an entire style sheet by adding .atom-text-editor
to the file name. This is now not necessary anymore and can be removed.
Before:
my-ui-theme/
+ styles/
+ index.atom-text-editor.less
+
After:
my-ui-theme/
+ styles/
+ index.less
+
By replacing atom-text-editor::shadow
with atom-text-editor.editor
, specificity might have changed. This can cause the side effect that some of your properties won't be strong enough. To fix that, you can increase specificity on your selector. A simple way is to just repeat your class (in the example below it's .my-class
):
Before:
atom-text-editor::shadow .my-class {
+ color: hotpink;
+}
+
After:
atom-text-editor .my-class.my-class {
+ color: hotpink;
+}
+
package.json
file to "engines": { "atom": ">=1.13.0 <2.0.0" }
. This will prevent Atom from updating your theme or package before the user also updates Atom to version 1.13
.1.13
reaches Stable. Check blog.atom.io to see if 1.13
already has been released. Don't worry if you're a bit late, Atom will transform the deprecated selectors automatically to avoid breaking any themes or packages. But users will start to see a deprecation warning in deprecation-cop.In Atom 1.13
the Shadow DOM got removed from text editors. For more background on the reasoning, check out the Pull Request where it was removed. In this guide you will learn how to migrate your theme or package.
Here is a quick summary to see all the changes at a glance:
Before | +/- | After |
---|---|---|
atom-text-editor::shadow {} | - ::shadow | atom-text-editor {} |
.class /deep/ .class {} | - /deep/ | .class .class {} |
atom-text-editor, :host {} | - :host | atom-text-editor {} |
.comment {} | + .syntax-- | .syntax--comment {} |
Below you'll find more detailed examples.
::shadow
Remove the ::shadow
pseudo-element selector from atom-text-editor
.
Before:
atom-text-editor::shadow .cursor {
+ border-color: hotpink;
+}
+
After:
atom-text-editor .cursor {
+ border-color: hotpink;
+}
+
/deep/
Remove the /deep/
combinator selector. It didn't get used that often, mainly to customize the scrollbars.
Before:
.scrollbars-visible-always /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
After:
.scrollbars-visible-always ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
:host
Remove the :host
pseudo-element selector. To scope any styles to the text editor, using atom-text-editor
is already enough.
Before:
atom-text-editor,
+:host {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
After:
atom-text-editor {
+ .cursor {
+ border-color: hotpink;
+ }
+}
+
syntax--
Add a syntax--
prefix to all grammar selectors used by language packages. It looks a bit verbose, but it will protect all the grammar selectors from accidental style pollution.
Before:
.comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .markup.link {
+ color: @mono-3;
+ }
+}
+
After:
.syntax--comment {
+ color: @mono-3;
+ font-style: italic;
+
+ .syntax--markup.syntax--link {
+ color: @mono-3;
+ }
+}
+
Note: Selectors like the .gutter
, .indent-guide
, .cursor
among others, that are also part of Syntax themes, don't need a prefix. Only grammar selectors that get used by language packages. For example .syntax--keyword
, .syntax--keyword.syntax--operator.syntax--js
.
Atom also allowed you to target a specific shadow DOM context with an entire style sheet by adding .atom-text-editor
to the file name. This is now not necessary anymore and can be removed.
Before:
my-ui-theme/
+ styles/
+ index.atom-text-editor.less
+
After:
my-ui-theme/
+ styles/
+ index.less
+
By replacing atom-text-editor::shadow
with atom-text-editor.editor
, specificity might have changed. This can cause the side effect that some of your properties won't be strong enough. To fix that, you can increase specificity on your selector. A simple way is to just repeat your class (in the example below it's .my-class
):
Before:
atom-text-editor::shadow .my-class {
+ color: hotpink;
+}
+
After:
atom-text-editor .my-class.my-class {
+ color: hotpink;
+}
+
package.json
file to "engines": { "atom": ">=1.13.0 <2.0.0" }
. This will prevent Atom from updating your theme or package before the user also updates Atom to version 1.13
.1.13
reaches Stable. Check blog.atom.io to see if 1.13
already has been released. Don't worry if you're a bit late, Atom will transform the deprecated selectors automatically to avoid breaking any themes or packages. But users will start to see a deprecation warning in deprecation-cop.STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, use this at your own risk. Current Pulsar documentation is found at documentation home.
Note
Note: Atom has been at v1.x for over a year, so this appendix is mostly obsolete at this point. We're retaining it for historic and reference purposes.
Atom is at 1.0! Much of the effort leading up to the 1.0 has been cleaning up APIs in an attempt to future proof, and make a more pleasant experience developing packages. If you have developed packages or syntaxes for Atom before the 1.0 API, you can find some tips on upgrading your work in this appendix.
This document will guide you through the large bits of upgrading your package to work with 1.0 APIs.
We've set deprecation messages and errors in strategic places to help make sure you don't miss anything. You should be able to get 95% of the way to an updated package just by fixing errors and deprecations. There are a couple of things you can do to get the full effect of all the errors and deprecations.
If you use any class from require 'atom'
with a $
or View
in the name, add the atom-space-pen-views
module to your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
Then run apm install
in your package directory.
Anywhere you are requiring one of the following from atom
you need to require them from atom-space-pen-views
instead.
# require these from 'atom-space-pen-views' rather than 'atom'
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
So this:
# Old way
+{$, TextEditorView, View, GitRepository} = require 'atom'
+
Would be replaced by this:
# New way
+{GitRepository} = require 'atom'
+{$, TextEditorView, View} = require 'atom-space-pen-views'
+
You wrote specs, right!? Here's where they shine. Run them with cmd-shift-P
, and search for run package specs
. It will show all the deprecation messages and errors.
When you are deprecation free and all done converting, upgrade the engines
field in your package.json:
{
+ "engines": {
+ "atom": ">=0.174.0 <2.0.0"
+ }
+}
+
We have upgraded all the core packages. Please see this issue for a link to all the upgrade PRs.
All of the methods in Atom core that have changes will emit deprecation messages when called. These messages are shown in two places: your package specs, and in Deprecation Cop.
Just run your specs, and all the deprecations will be displayed in yellow.
Note
Note: Deprecations are only displayed when executing specs through the "Window: Run Package Specs" command in the Atom UI. Deprecations are not displayed when running specs at the terminal.
Run Atom in Dev Mode, atom --dev
, with your package loaded, and open Deprecation Cop (search for "deprecation" in the command palette). Deprecated methods will appear in Deprecation Cop only after they have been called.
When Deprecation Cop is open, and deprecated methods are called, a Refresh
button will appear in the top right of the Deprecation Cop interface. So exercise your package, then come back to Deprecation Cop and click the Refresh
button.
Previous to 1.0, views were baked into Atom core. These views were based on jQuery and space-pen
. They looked something like this:
# The old way: getting views from atom
+{$, TextEditorView, View} = require 'atom'
+
+module.exports =
+class SomeView extends View
+ @content: ->
+ @div class: 'find-and-replace', =>
+ @div class: 'block', =>
+ @subview 'myEditor', new TextEditorView(mini: true)
+ #...
+
require 'atom'
no longer provides view helpers or jQuery. Atom Core is now 'view agnostic'. The preexisting view system is available from a new Node module: atom-space-pen-views.
atom-space-pen-views now provides jQuery, space-pen views, and Atom specific views:
# These are now provided by atom-space-pen-views
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
To use the new views, you need to specify the atom-space-pen-views module in your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
space-pen bundles jQuery. If you do not need space-pen or any of the views, you can require jQuery directly.
{
+ "dependencies": {
+ "jquery": "^2"
+ }
+}
+
Sometimes it is as simple as converting the requires at the top of each view page. I assume you read the 'TL;DR' section and have updated all of your requires.
afterAttach
and beforeRemove
updatedThe afterAttach
and beforeRemove
hooks have been replaced with attached
and detached
and the semantics have changed.
afterAttach
was called whenever the node was attached to another DOM node, even if that parent node wasn't present in the DOM. afterAttach
also was called with a boolean indicating whether or not the element and its parents were on the DOM. Now the attached
hook is only called when the node and all of its parents are actually on the DOM, and is not called with a boolean.
beforeRemove
was only called when $.fn.remove
was called, which was typically used when the node was completely removed from the DOM. The new detached
hook is called whenever the DOM node is detached, which could happen if the node is being detached for reattachment later. In short, if beforeRemove
is called the node is never coming back. With detached
it might be attached again later.
# Old way
+{View} = require 'atom'
+class MyView extends View
+ afterAttach: (onDom) ->
+ #...
+
+ beforeRemove: ->
+ #...
+
# New way
+{View} = require 'atom-space-pen-views'
+class MyView extends View
+ attached: ->
+ # Always called with the equivalent of @afterAttach(true)!
+ #...
+
+ detached: ->
+ #...
+
subscribe
and subscribeToCommand
methods removedThe subscribe
and subscribeToCommand
methods have been removed. See the Eventing and Disposables section for more info.
All of the atom-specific methods available on the TextEditorView
have been moved to the TextEditor
, available via TextEditorView::getModel
. See the TextEditorView
docs and TextEditor
docs for more info.
The ScrollView
has very minor changes.
You can no longer use @off
to remove default behavior for core:move-up
, core:move-down
, etc.
# Old way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ super()
+ # turn off default scrolling behavior from ScrollView
+ @off 'core:move-up'
+ @off 'core:move-down'
+ @off 'core:move-left'
+ @off 'core:move-right'
+
# New way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ disposable = super()
+ # turn off default scrolling behavior from ScrollView
+ disposable.dispose()
+
Your SelectListView
might look something like this:
# Old!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ @addClass('command-palette overlay from-top')
+ atom.workspaceView.command 'command-palette:toggle', => @toggle()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ if @hasParent()
+ @cancel()
+ else
+ @attach()
+
+ attach: ->
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ atom.workspaceView.append(this)
+ @focusFilterEditor()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+
This attaches and detaches itself from the DOM when toggled, canceling magically detaches it from the DOM, and it uses the classes overlay
and from-top
.
The new SelectListView no longer automatically detaches itself from the DOM when cancelled. It's up to you to implement whatever cancel behavior you want. Using the new APIs to mimic the semantics of the old class, it should look like this:
# New!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ # no more need for the `overlay` and `from-top` classes
+ @addClass('command-palette')
+ atom.commands.add 'atom-workspace', 'command-palette:toggle', => @toggle()
+
+ # You need to implement the `cancelled` method and hide.
+ cancelled: ->
+ @hide()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ # Toggling now checks panel visibility,
+ # and hides / shows rather than attaching to / detaching from the DOM.
+ if @panel?.isVisible()
+ @cancel()
+ else
+ @show()
+
+ show: ->
+ # Now you will add your select list as a modal panel to the workspace
+ @panel ?= atom.workspace.addModalPanel(item: this)
+ @panel.show()
+
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ @focusFilterEditor()
+
+ hide: ->
+ @panel?.hide()
+
CommandPaletteView
as a real-world example.SelectListView
docs for all options.The API no longer exposes any specialized view objects or view classes. atom.workspaceView
, and all the view classes: WorkspaceView
, EditorView
, PaneView
, etc. have been globally deprecated.
Nearly all of the atom-specific actions performed by the old view objects can now be managed via the model layer. For example, here's adding a panel to the interface using the atom.workspace
model instead of the workspaceView
:
# Old!
+div = document.createElement('div')
+atom.workspaceView.appendToTop(div)
+
# New!
+div = document.createElement('div')
+atom.workspace.addTopPanel(item: div)
+
For actions that still require the view, such as dispatching commands or munging css classes, you'll access the view via the atom.views.getView()
method. This will return a subclass of HTMLElement
rather than a jQuery object or an instance of a deprecated view class (e.g. WorkspaceView
).
# Old!
+workspaceView = atom.workspaceView
+editorView = workspaceView.getActiveEditorView()
+paneView = editorView.getPaneView()
+
# New!
+# Generally, just use the models
+workspace = atom.workspace
+editor = workspace.getActiveTextEditor()
+pane = editor.getPane()
+
+# If you need views, get them with `getView`
+workspaceElement = atom.views.getView(atom.workspace)
+editorElement = atom.views.getView(editor)
+paneElement = atom.views.getView(pane)
+
atom.workspaceView
, the WorkspaceView
class and the EditorView
class have been deprecated. These two objects are used heavily throughout specs, mostly to dispatch events and commands. This section will explain how to remove them while still retaining the ability to dispatch events and commands.
WorkspaceView
referencesWorkspaceView
has been deprecated. Everything you could do on the view, you can now do on the Workspace
model.
Requiring WorkspaceView
from atom
and accessing any methods on it will throw a deprecation warning. Many specs lean heavily on WorkspaceView
to trigger commands and fetch EditorView
objects.
Your specs might contain something like this:
# Old!
+{WorkspaceView} = require 'atom'
+describe 'FindView', ->
+ beforeEach ->
+ atom.workspaceView = new WorkspaceView()
+
Instead, we will use the atom.views.getView()
method. This will return a plain HTMLElement
, not a WorkspaceView
or jQuery object.
# New!
+describe 'FindView', ->
+ workspaceElement = null
+ beforeEach ->
+ workspaceElement = atom.views.getView(atom.workspace)
+
The workspace needs to be attached to the DOM in some cases. For example, view hooks only work (attached()
on View
, attachedCallback()
on custom elements) when there is a descendant attached to the DOM.
You might see this in your specs:
# Old!
+atom.workspaceView.attachToDom()
+
Change it to:
# New!
+jasmine.attachToDOM(workspaceElement)
+
Like WorkspaceView
, EditorView
has been deprecated. Everything you needed to do on the view you are now able to do on the TextEditor
model.
In many cases, you will not even need to get the editor's view anymore. Any of those instances should be updated to use the TextEditor
instance instead. You should really only need the editor's view when you plan on triggering a command on the view in a spec.
Your specs might contain something like this:
# Old!
+describe 'Something', ->
+ [editorView] = []
+ beforeEach ->
+ editorView = atom.workspaceView.getActiveView()
+
We're going to use atom.views.getView()
again to get the editor element. As in the case of the workspaceElement
, getView
will return a subclass of HTMLElement
rather than an EditorView
or jQuery object.
# New!
+describe 'Something', ->
+ [editor, editorElement] = []
+ beforeEach ->
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
Since the editorElement
objects are no longer jQuery objects, they no longer support trigger()
. Additionally, Atom has a new command dispatcher, atom.commands
, that we use rather than commandeering jQuery's trigger
method.
From this:
# Old!
+workspaceView.trigger 'a-package:toggle'
+editorView.trigger 'find-and-replace:show'
+
To this:
# New!
+atom.commands.dispatch workspaceElement, 'a-package:toggle'
+atom.commands.dispatch editorElement, 'find-and-replace:show'
+
A couple large things changed with respect to events:
Disposable
objectssubscribe()
method is no longer available on space-pen
View
objectsrequire 'atom'
All events from the Atom API are now methods that return a Disposable
object, on which you can call dispose()
to unsubscribe.
# Old!
+editor.on 'changed', ->
+
# New!
+disposable = editor.onDidChange ->
+
+# You can unsubscribe at some point in the future via `dispose()`
+disposable.dispose()
+
Deprecation warnings will guide you toward the correct methods.
CompositeDisposable
You can group multiple disposables into a single disposable with a CompositeDisposable
.
{CompositeDisposable} = require 'atom'
+
+class Something
+ constructor: ->
+ editor = atom.workspace.getActiveTextEditor()
+ @disposables = new CompositeDisposable
+ @disposables.add editor.onDidChange ->
+ @disposables.add editor.onDidChangePath ->
+
+ destroy: ->
+ @disposables.dispose()
+
View::subscribe
and Subscriber::subscribe
callsThere were a couple permutations of subscribe()
. In these examples, a CompositeDisposable
is used as it will commonly be useful where conversion is necessary.
subscribe(unsubscribable)
This one is very straight forward.
# Old!
+@subscribe editor.on 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(modelObject, event, method)
When the modelObject is an Atom model object, the change is very simple. Just use the correct event method, and add it to your CompositeDisposable.
# Old!
+@subscribe editor, 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(jQueryObject, selector(optional), event, method)
Things are a little more complicated when subscribing to a DOM or jQuery element. Atom no longer provides helpers for subscribing to elements. You can use jQuery or the native DOM APIs, whichever you prefer.
# Old!
+@subscribe $(window), 'focus', ->
+
# New!
+{Disposable, CompositeDisposable} = require 'atom'
+disposables = new CompositeDisposable
+
+# New with jQuery
+focusCallback = ->
+$(window).on 'focus', focusCallback
+disposables.add new Disposable ->
+ $(window).off 'focus', focusCallback
+
+# New with native APIs
+focusCallback = ->
+window.addEventListener 'focus', focusCallback
+disposables.add new Disposable ->
+ window.removeEventListener 'focus', focusCallback
+
Emitter
You no longer need to require emissary
to get an emitter. We now provide an Emitter
class from require 'atom'
. We have a specific pattern for use of the Emitter
. Rather than mixing it in, we instantiate a member variable, and create explicit subscription methods. For more information see the Emitter
docs.
# New!
+{Emitter} = require 'atom'
+
+class Something
+ constructor: ->
+ @emitter = new Emitter
+
+ destroy: ->
+ @emitter.dispose()
+
+ onDidChange: (callback) ->
+ @emitter.on 'did-change', callback
+
+ methodThatFiresAChange: ->
+ @emitter.emit 'did-change', {data: 2}
+
+# Using the evented class
+something = new Something
+something.onDidChange (eventObject) ->
+ console.log eventObject.data # => 2
+something.methodThatFiresAChange()
+
$.fn.command
and View::subscribeToCommand
are no longer available. Now we use atom.commands.add
, and collect the results in a CompositeDisposable
. See the docs for more info.
# Old!
+atom.workspaceView.command 'core:close core:cancel', ->
+
+# When inside a View class, you might see this
+@subscribeToCommand 'core:close core:cancel', ->
+
# New!
+@disposables.add atom.commands.add 'atom-workspace',
+ 'core:close': ->
+ 'core:cancel': ->
+
+# You can register commands directly on individual DOM elements in addition to
+# using selectors. When in a View class, you should have a `@element` object
+# available. `@element` is a plain HTMLElement object
+@disposables.add atom.commands.add @element,
+ 'core:close': ->
+ 'core:cancel': ->
+
Many selectors have changed, and we have introduced the Shadow DOM to the editor. See the Upgrading Your UI Theme And Package Selectors guide for more information in upgrading your package stylesheets.
Note
Note: The Shadow DOM was removed in Atom 1.13
. The ::shadow
and /deep/
selectors and the context-targeted style sheets described below won't work and should not be used anymore.
In addition to changes in Atom's scripting API, we'll also be making some breaking changes to Atom's DOM structure, requiring style sheets and keymaps in both packages and themes to be updated.
Deprecation Cop will list usages of deprecated selector patterns to guide you. You can access it via the Command Palette (cmd-shift-p
, then search for Deprecation
). It breaks the deprecations down by package:
Rather than adding classes to standard HTML elements to indicate their role, Atom now uses custom element names. For example, <div class="workspace">
has now been replaced with <atom-workspace>
. Selectors should be updated accordingly. Note that tag names have lower specificity than classes in CSS, so you'll need to take care in converting things.
Old Selector | New Selector |
---|---|
.editor | atom-text-editor |
.editor.mini | atom-text-editor[mini] |
.workspace | atom-workspace |
.horizontal | atom-workspace-axis.horizontal |
.vertical | atom-workspace-axis.vertical |
.pane-container | atom-pane-container |
.pane | atom-pane |
.tool-panel | atom-panel |
.panel-top | atom-panel.top |
.panel-bottom | atom-panel.bottom |
.panel-left | atom-panel.left |
.panel-right | atom-panel.right |
.overlay | atom-panel.modal |
Text editor content is now rendered in the shadow DOM, which shields it from being styled by global style sheets to protect against accidental style pollution. For more background on the shadow DOM, check out the Shadow DOM 101 on HTML 5 Rocks. If you need to style text editor content in a UI theme, you'll need to circumvent this protection for any rules that target the text editor's content. Some examples of the kinds of UI theme styles needing to be updated:
.editor
During a transition phase, it will be possible to enable or disable the text editor's shadow DOM in the settings, so themes will need to be compatible with both approaches.
Chromium provides two tools for bypassing shadow boundaries, the ::shadow
pseudo-element and the /deep/
combinator. For an in-depth explanation of styling the shadow DOM, see the Shadow DOM 201 article on HTML 5 Rocks.
::shadow
The ::shadow
pseudo-element allows you to bypass a single shadow root. For example, say you want to update a highlight decoration for a linter package. Initially, the style looks as follows:
// Without shadow DOM support
+atom-text-editor .highlight.my-linter {
+ background: hotpink;
+}
+
In order for this style to apply with the shadow DOM enabled, you will need to add a second selector with the ::shadow
pseudo-element. You should leave the original selector in place so your theme continues to work with the shadow DOM disabled during the transition period.
// With shadow DOM support
+atom-text-editor .highlight.my-linter,
+atom-text-editor::shadow .highlight.my-linter {
+ background: hotpink;
+}
+
Check out the find-and-replace package for another example of using ::shadow
to pierce the shadow DOM.
/deep/
The /deep/
combinator overrides all shadow boundaries, making it useful for rules you want to apply globally such as scrollbar styling. Here's a snippet containing scrollbar styling for the Atom Dark UI theme before shadow DOM support:
// Without shadow DOM support
+.scrollbars-visible-always {
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track,
+ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
To style scrollbars even inside of the shadow DOM, each rule needs to be prefixed with /deep/
. We use /deep/
instead of ::shadow
because we don't care about the selector of the host element in this case. We just want our styling to apply everywhere.
// With shadow DOM support using /deep/
+.scrollbars-visible-always {
+ /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ /deep/ ::-webkit-scrollbar-track,
+ /deep/ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ /deep/ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
The selector features discussed above allow you to target shadow DOM content with specific selectors, but Atom also allows you to target a specific shadow DOM context with an entire style sheet. The context into which a style sheet is loaded is based on the file name. If you want to load a style sheet into the editor, name it with the .atom-text-editor.less
or .atom-text-editor.css
extensions.
my-ui-theme/
+ styles/
+ index.less # loaded globally
+ index.atom-text-editor.less # loaded in the text editor shadow DOM
+
Check out this style sheet from the decoration-example package for an example of context-targeting.
Inside a context-targeted style sheet, there's no need to use the ::shadow
or /deep/
expressions. If you want to refer to the element containing the shadow root, you can use the ::host
pseudo-element.
During the transition phase, style sheets targeting the atom-text-editor
context will also be loaded globally. Make sure you update your selectors in a way that maintains compatibility with the shadow DOM being disabled. That means if you use a ::host
pseudo element, you should also include the same style rule matches against atom-text-editor
.
Note
Note: The Shadow DOM was removed in Atom 1.13
. The :host
selector described below won't work and should not be used anymore.
Text editor content is now rendered in the shadow DOM, which shields it from being styled by global style sheets to protect against accidental style pollution. For more background on the shadow DOM, check out the Shadow DOM 101 on HTML 5 Rocks.
Syntax themes are specifically intended to style only text editor content, so they are automatically loaded directly into the text editor's shadow DOM when it is enabled. This happens automatically when the theme's package.json
contains a theme: "syntax"
declaration, so you don't need to change anything to target the appropriate context.
When theme style sheets are loaded into the text editor's shadow DOM, selectors intended to target the editor from the outside no longer make sense. Styles targeting the .editor
and .editor-colors
classes instead need to target the :host
pseudo-element, which matches against the containing atom-text-editor
node. Check out the Shadow DOM 201 article for more information about the :host
pseudo-element.
Here's an example from Atom's light syntax theme. Note that the atom-text-editor
selector intended to target the editor from the outside has been retained to allow the theme to keep working during the transition phase when it is possible to disable the shadow DOM.
atom-text-editor,
+:host {
+ /* :host added */
+ background-color: @syntax-background-color;
+ color: @syntax-text-color;
+
+ .invisible-character {
+ color: @syntax-invisible-character-color;
+ }
+ /* more nested selectors... */
+}
+
This document will guide you through the large bits of upgrading your package to work with 1.0 APIs.
We've set deprecation messages and errors in strategic places to help make sure you don't miss anything. You should be able to get 95% of the way to an updated package just by fixing errors and deprecations. There are a couple of things you can do to get the full effect of all the errors and deprecations.
If you use any class from require 'atom'
with a $
or View
in the name, add the atom-space-pen-views
module to your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
Then run apm install
in your package directory.
Anywhere you are requiring one of the following from atom
you need to require them from atom-space-pen-views
instead.
# require these from 'atom-space-pen-views' rather than 'atom'
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
So this:
# Old way
+{$, TextEditorView, View, GitRepository} = require 'atom'
+
Would be replaced by this:
# New way
+{GitRepository} = require 'atom'
+{$, TextEditorView, View} = require 'atom-space-pen-views'
+
You wrote specs, right!? Here's where they shine. Run them with cmd-shift-P
, and search for run package specs
. It will show all the deprecation messages and errors.
When you are deprecation free and all done converting, upgrade the engines
field in your package.json:
{
+ "engines": {
+ "atom": ">=0.174.0 <2.0.0"
+ }
+}
+
We have upgraded all the core packages. Please see this issue for a link to all the upgrade PRs.
All of the methods in Atom core that have changes will emit deprecation messages when called. These messages are shown in two places: your package specs, and in Deprecation Cop.
Just run your specs, and all the deprecations will be displayed in yellow.
Note
Note: Deprecations are only displayed when executing specs through the "Window: Run Package Specs" command in the Atom UI. Deprecations are not displayed when running specs at the terminal.
Run Atom in Dev Mode, atom --dev
, with your package loaded, and open Deprecation Cop (search for "deprecation" in the command palette). Deprecated methods will appear in Deprecation Cop only after they have been called.
When Deprecation Cop is open, and deprecated methods are called, a Refresh
button will appear in the top right of the Deprecation Cop interface. So exercise your package, then come back to Deprecation Cop and click the Refresh
button.
Previous to 1.0, views were baked into Atom core. These views were based on jQuery and space-pen
. They looked something like this:
# The old way: getting views from atom
+{$, TextEditorView, View} = require 'atom'
+
+module.exports =
+class SomeView extends View
+ @content: ->
+ @div class: 'find-and-replace', =>
+ @div class: 'block', =>
+ @subview 'myEditor', new TextEditorView(mini: true)
+ #...
+
require 'atom'
no longer provides view helpers or jQuery. Atom Core is now 'view agnostic'. The preexisting view system is available from a new Node module: atom-space-pen-views.
atom-space-pen-views now provides jQuery, space-pen views, and Atom specific views:
# These are now provided by atom-space-pen-views
+$
+$$
+$$$
+View
+TextEditorView
+ScrollView
+SelectListView
+
To use the new views, you need to specify the atom-space-pen-views module in your package's package.json
file's dependencies:
{
+ "dependencies": {
+ "atom-space-pen-views": "^2.0.3"
+ }
+}
+
space-pen bundles jQuery. If you do not need space-pen or any of the views, you can require jQuery directly.
{
+ "dependencies": {
+ "jquery": "^2"
+ }
+}
+
Sometimes it is as simple as converting the requires at the top of each view page. I assume you read the 'TL;DR' section and have updated all of your requires.
afterAttach
and beforeRemove
updatedThe afterAttach
and beforeRemove
hooks have been replaced with attached
and detached
and the semantics have changed.
afterAttach
was called whenever the node was attached to another DOM node, even if that parent node wasn't present in the DOM. afterAttach
also was called with a boolean indicating whether or not the element and its parents were on the DOM. Now the attached
hook is only called when the node and all of its parents are actually on the DOM, and is not called with a boolean.
beforeRemove
was only called when $.fn.remove
was called, which was typically used when the node was completely removed from the DOM. The new detached
hook is called whenever the DOM node is detached, which could happen if the node is being detached for reattachment later. In short, if beforeRemove
is called the node is never coming back. With detached
it might be attached again later.
# Old way
+{View} = require 'atom'
+class MyView extends View
+ afterAttach: (onDom) ->
+ #...
+
+ beforeRemove: ->
+ #...
+
# New way
+{View} = require 'atom-space-pen-views'
+class MyView extends View
+ attached: ->
+ # Always called with the equivalent of @afterAttach(true)!
+ #...
+
+ detached: ->
+ #...
+
subscribe
and subscribeToCommand
methods removedThe subscribe
and subscribeToCommand
methods have been removed. See the Eventing and Disposables section for more info.
All of the atom-specific methods available on the TextEditorView
have been moved to the TextEditor
, available via TextEditorView::getModel
. See the TextEditorView
docs and TextEditor
docs for more info.
The ScrollView
has very minor changes.
You can no longer use @off
to remove default behavior for core:move-up
, core:move-down
, etc.
# Old way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ super()
+ # turn off default scrolling behavior from ScrollView
+ @off 'core:move-up'
+ @off 'core:move-down'
+ @off 'core:move-left'
+ @off 'core:move-right'
+
# New way to turn off default behavior
+class ResultsView extends ScrollView
+ initialize: (@model) ->
+ disposable = super()
+ # turn off default scrolling behavior from ScrollView
+ disposable.dispose()
+
Your SelectListView
might look something like this:
# Old!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ @addClass('command-palette overlay from-top')
+ atom.workspaceView.command 'command-palette:toggle', => @toggle()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ if @hasParent()
+ @cancel()
+ else
+ @attach()
+
+ attach: ->
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ atom.workspaceView.append(this)
+ @focusFilterEditor()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+
This attaches and detaches itself from the DOM when toggled, canceling magically detaches it from the DOM, and it uses the classes overlay
and from-top
.
The new SelectListView no longer automatically detaches itself from the DOM when cancelled. It's up to you to implement whatever cancel behavior you want. Using the new APIs to mimic the semantics of the old class, it should look like this:
# New!
+class CommandPaletteView extends SelectListView
+ initialize: ->
+ super()
+ # no more need for the `overlay` and `from-top` classes
+ @addClass('command-palette')
+ atom.commands.add 'atom-workspace', 'command-palette:toggle', => @toggle()
+
+ # You need to implement the `cancelled` method and hide.
+ cancelled: ->
+ @hide()
+
+ confirmed: ({name, jQuery}) ->
+ @cancel()
+ # do something with the result
+
+ toggle: ->
+ # Toggling now checks panel visibility,
+ # and hides / shows rather than attaching to / detaching from the DOM.
+ if @panel?.isVisible()
+ @cancel()
+ else
+ @show()
+
+ show: ->
+ # Now you will add your select list as a modal panel to the workspace
+ @panel ?= atom.workspace.addModalPanel(item: this)
+ @panel.show()
+
+ @storeFocusedElement()
+
+ items = [] # TODO: build items
+ @setItems(items)
+
+ @focusFilterEditor()
+
+ hide: ->
+ @panel?.hide()
+
CommandPaletteView
as a real-world example.SelectListView
docs for all options.The API no longer exposes any specialized view objects or view classes. atom.workspaceView
, and all the view classes: WorkspaceView
, EditorView
, PaneView
, etc. have been globally deprecated.
Nearly all of the atom-specific actions performed by the old view objects can now be managed via the model layer. For example, here's adding a panel to the interface using the atom.workspace
model instead of the workspaceView
:
# Old!
+div = document.createElement('div')
+atom.workspaceView.appendToTop(div)
+
# New!
+div = document.createElement('div')
+atom.workspace.addTopPanel(item: div)
+
For actions that still require the view, such as dispatching commands or munging css classes, you'll access the view via the atom.views.getView()
method. This will return a subclass of HTMLElement
rather than a jQuery object or an instance of a deprecated view class (e.g. WorkspaceView
).
# Old!
+workspaceView = atom.workspaceView
+editorView = workspaceView.getActiveEditorView()
+paneView = editorView.getPaneView()
+
# New!
+# Generally, just use the models
+workspace = atom.workspace
+editor = workspace.getActiveTextEditor()
+pane = editor.getPane()
+
+# If you need views, get them with `getView`
+workspaceElement = atom.views.getView(atom.workspace)
+editorElement = atom.views.getView(editor)
+paneElement = atom.views.getView(pane)
+
atom.workspaceView
, the WorkspaceView
class and the EditorView
class have been deprecated. These two objects are used heavily throughout specs, mostly to dispatch events and commands. This section will explain how to remove them while still retaining the ability to dispatch events and commands.
WorkspaceView
referencesWorkspaceView
has been deprecated. Everything you could do on the view, you can now do on the Workspace
model.
Requiring WorkspaceView
from atom
and accessing any methods on it will throw a deprecation warning. Many specs lean heavily on WorkspaceView
to trigger commands and fetch EditorView
objects.
Your specs might contain something like this:
# Old!
+{WorkspaceView} = require 'atom'
+describe 'FindView', ->
+ beforeEach ->
+ atom.workspaceView = new WorkspaceView()
+
Instead, we will use the atom.views.getView()
method. This will return a plain HTMLElement
, not a WorkspaceView
or jQuery object.
# New!
+describe 'FindView', ->
+ workspaceElement = null
+ beforeEach ->
+ workspaceElement = atom.views.getView(atom.workspace)
+
The workspace needs to be attached to the DOM in some cases. For example, view hooks only work (attached()
on View
, attachedCallback()
on custom elements) when there is a descendant attached to the DOM.
You might see this in your specs:
# Old!
+atom.workspaceView.attachToDom()
+
Change it to:
# New!
+jasmine.attachToDOM(workspaceElement)
+
Like WorkspaceView
, EditorView
has been deprecated. Everything you needed to do on the view you are now able to do on the TextEditor
model.
In many cases, you will not even need to get the editor's view anymore. Any of those instances should be updated to use the TextEditor
instance instead. You should really only need the editor's view when you plan on triggering a command on the view in a spec.
Your specs might contain something like this:
# Old!
+describe 'Something', ->
+ [editorView] = []
+ beforeEach ->
+ editorView = atom.workspaceView.getActiveView()
+
We're going to use atom.views.getView()
again to get the editor element. As in the case of the workspaceElement
, getView
will return a subclass of HTMLElement
rather than an EditorView
or jQuery object.
# New!
+describe 'Something', ->
+ [editor, editorElement] = []
+ beforeEach ->
+ editor = atom.workspace.getActiveTextEditor()
+ editorElement = atom.views.getView(editor)
+
Since the editorElement
objects are no longer jQuery objects, they no longer support trigger()
. Additionally, Atom has a new command dispatcher, atom.commands
, that we use rather than commandeering jQuery's trigger
method.
From this:
# Old!
+workspaceView.trigger 'a-package:toggle'
+editorView.trigger 'find-and-replace:show'
+
To this:
# New!
+atom.commands.dispatch workspaceElement, 'a-package:toggle'
+atom.commands.dispatch editorElement, 'find-and-replace:show'
+
A couple large things changed with respect to events:
Disposable
objectssubscribe()
method is no longer available on space-pen
View
objectsrequire 'atom'
All events from the Atom API are now methods that return a Disposable
object, on which you can call dispose()
to unsubscribe.
# Old!
+editor.on 'changed', ->
+
# New!
+disposable = editor.onDidChange ->
+
+# You can unsubscribe at some point in the future via `dispose()`
+disposable.dispose()
+
Deprecation warnings will guide you toward the correct methods.
CompositeDisposable
You can group multiple disposables into a single disposable with a CompositeDisposable
.
{CompositeDisposable} = require 'atom'
+
+class Something
+ constructor: ->
+ editor = atom.workspace.getActiveTextEditor()
+ @disposables = new CompositeDisposable
+ @disposables.add editor.onDidChange ->
+ @disposables.add editor.onDidChangePath ->
+
+ destroy: ->
+ @disposables.dispose()
+
View::subscribe
and Subscriber::subscribe
callsThere were a couple permutations of subscribe()
. In these examples, a CompositeDisposable
is used as it will commonly be useful where conversion is necessary.
subscribe(unsubscribable)
This one is very straight forward.
# Old!
+@subscribe editor.on 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(modelObject, event, method)
When the modelObject is an Atom model object, the change is very simple. Just use the correct event method, and add it to your CompositeDisposable.
# Old!
+@subscribe editor, 'changed', ->
+
# New!
+disposables = new CompositeDisposable
+disposables.add editor.onDidChange ->
+
subscribe(jQueryObject, selector(optional), event, method)
Things are a little more complicated when subscribing to a DOM or jQuery element. Atom no longer provides helpers for subscribing to elements. You can use jQuery or the native DOM APIs, whichever you prefer.
# Old!
+@subscribe $(window), 'focus', ->
+
# New!
+{Disposable, CompositeDisposable} = require 'atom'
+disposables = new CompositeDisposable
+
+# New with jQuery
+focusCallback = ->
+$(window).on 'focus', focusCallback
+disposables.add new Disposable ->
+ $(window).off 'focus', focusCallback
+
+# New with native APIs
+focusCallback = ->
+window.addEventListener 'focus', focusCallback
+disposables.add new Disposable ->
+ window.removeEventListener 'focus', focusCallback
+
Emitter
You no longer need to require emissary
to get an emitter. We now provide an Emitter
class from require 'atom'
. We have a specific pattern for use of the Emitter
. Rather than mixing it in, we instantiate a member variable, and create explicit subscription methods. For more information see the Emitter
docs.
# New!
+{Emitter} = require 'atom'
+
+class Something
+ constructor: ->
+ @emitter = new Emitter
+
+ destroy: ->
+ @emitter.dispose()
+
+ onDidChange: (callback) ->
+ @emitter.on 'did-change', callback
+
+ methodThatFiresAChange: ->
+ @emitter.emit 'did-change', {data: 2}
+
+# Using the evented class
+something = new Something
+something.onDidChange (eventObject) ->
+ console.log eventObject.data # => 2
+something.methodThatFiresAChange()
+
$.fn.command
and View::subscribeToCommand
are no longer available. Now we use atom.commands.add
, and collect the results in a CompositeDisposable
. See the docs for more info.
# Old!
+atom.workspaceView.command 'core:close core:cancel', ->
+
+# When inside a View class, you might see this
+@subscribeToCommand 'core:close core:cancel', ->
+
# New!
+@disposables.add atom.commands.add 'atom-workspace',
+ 'core:close': ->
+ 'core:cancel': ->
+
+# You can register commands directly on individual DOM elements in addition to
+# using selectors. When in a View class, you should have a `@element` object
+# available. `@element` is a plain HTMLElement object
+@disposables.add atom.commands.add @element,
+ 'core:close': ->
+ 'core:cancel': ->
+
Many selectors have changed, and we have introduced the Shadow DOM to the editor. See the Upgrading Your UI Theme And Package Selectors guide for more information in upgrading your package stylesheets.
Note
Note: The Shadow DOM was removed in Atom 1.13
. The :host
selector described below won't work and should not be used anymore.
Text editor content is now rendered in the shadow DOM, which shields it from being styled by global style sheets to protect against accidental style pollution. For more background on the shadow DOM, check out the Shadow DOM 101 on HTML 5 Rocks.
Syntax themes are specifically intended to style only text editor content, so they are automatically loaded directly into the text editor's shadow DOM when it is enabled. This happens automatically when the theme's package.json
contains a theme: "syntax"
declaration, so you don't need to change anything to target the appropriate context.
When theme style sheets are loaded into the text editor's shadow DOM, selectors intended to target the editor from the outside no longer make sense. Styles targeting the .editor
and .editor-colors
classes instead need to target the :host
pseudo-element, which matches against the containing atom-text-editor
node. Check out the Shadow DOM 201 article for more information about the :host
pseudo-element.
Here's an example from Atom's light syntax theme. Note that the atom-text-editor
selector intended to target the editor from the outside has been retained to allow the theme to keep working during the transition phase when it is possible to disable the shadow DOM.
atom-text-editor,
+:host {
+ /* :host added */
+ background-color: @syntax-background-color;
+ color: @syntax-text-color;
+
+ .invisible-character {
+ color: @syntax-invisible-character-color;
+ }
+ /* more nested selectors... */
+}
+
Note
Note: The Shadow DOM was removed in Atom 1.13
. The ::shadow
and /deep/
selectors and the context-targeted style sheets described below won't work and should not be used anymore.
In addition to changes in Atom's scripting API, we'll also be making some breaking changes to Atom's DOM structure, requiring style sheets and keymaps in both packages and themes to be updated.
Deprecation Cop will list usages of deprecated selector patterns to guide you. You can access it via the Command Palette (cmd-shift-p
, then search for Deprecation
). It breaks the deprecations down by package:
Rather than adding classes to standard HTML elements to indicate their role, Atom now uses custom element names. For example, <div class="workspace">
has now been replaced with <atom-workspace>
. Selectors should be updated accordingly. Note that tag names have lower specificity than classes in CSS, so you'll need to take care in converting things.
Old Selector | New Selector |
---|---|
.editor | atom-text-editor |
.editor.mini | atom-text-editor[mini] |
.workspace | atom-workspace |
.horizontal | atom-workspace-axis.horizontal |
.vertical | atom-workspace-axis.vertical |
.pane-container | atom-pane-container |
.pane | atom-pane |
.tool-panel | atom-panel |
.panel-top | atom-panel.top |
.panel-bottom | atom-panel.bottom |
.panel-left | atom-panel.left |
.panel-right | atom-panel.right |
.overlay | atom-panel.modal |
Text editor content is now rendered in the shadow DOM, which shields it from being styled by global style sheets to protect against accidental style pollution. For more background on the shadow DOM, check out the Shadow DOM 101 on HTML 5 Rocks. If you need to style text editor content in a UI theme, you'll need to circumvent this protection for any rules that target the text editor's content. Some examples of the kinds of UI theme styles needing to be updated:
.editor
During a transition phase, it will be possible to enable or disable the text editor's shadow DOM in the settings, so themes will need to be compatible with both approaches.
Chromium provides two tools for bypassing shadow boundaries, the ::shadow
pseudo-element and the /deep/
combinator. For an in-depth explanation of styling the shadow DOM, see the Shadow DOM 201 article on HTML 5 Rocks.
::shadow
The ::shadow
pseudo-element allows you to bypass a single shadow root. For example, say you want to update a highlight decoration for a linter package. Initially, the style looks as follows:
// Without shadow DOM support
+atom-text-editor .highlight.my-linter {
+ background: hotpink;
+}
+
In order for this style to apply with the shadow DOM enabled, you will need to add a second selector with the ::shadow
pseudo-element. You should leave the original selector in place so your theme continues to work with the shadow DOM disabled during the transition period.
// With shadow DOM support
+atom-text-editor .highlight.my-linter,
+atom-text-editor::shadow .highlight.my-linter {
+ background: hotpink;
+}
+
Check out the find-and-replace package for another example of using ::shadow
to pierce the shadow DOM.
/deep/
The /deep/
combinator overrides all shadow boundaries, making it useful for rules you want to apply globally such as scrollbar styling. Here's a snippet containing scrollbar styling for the Atom Dark UI theme before shadow DOM support:
// Without shadow DOM support
+.scrollbars-visible-always {
+ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ ::-webkit-scrollbar-track,
+ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
To style scrollbars even inside of the shadow DOM, each rule needs to be prefixed with /deep/
. We use /deep/
instead of ::shadow
because we don't care about the selector of the host element in this case. We just want our styling to apply everywhere.
// With shadow DOM support using /deep/
+.scrollbars-visible-always {
+ /deep/ ::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+ }
+
+ /deep/ ::-webkit-scrollbar-track,
+ /deep/ ::-webkit-scrollbar-corner {
+ background: @scrollbar-background-color;
+ }
+
+ /deep/ ::-webkit-scrollbar-thumb {
+ background: @scrollbar-color;
+ border-radius: 5px;
+ box-shadow: 0 0 1px black inset;
+ }
+}
+
The selector features discussed above allow you to target shadow DOM content with specific selectors, but Atom also allows you to target a specific shadow DOM context with an entire style sheet. The context into which a style sheet is loaded is based on the file name. If you want to load a style sheet into the editor, name it with the .atom-text-editor.less
or .atom-text-editor.css
extensions.
my-ui-theme/
+ styles/
+ index.less # loaded globally
+ index.atom-text-editor.less # loaded in the text editor shadow DOM
+
Check out this style sheet from the decoration-example package for an example of context-targeting.
Inside a context-targeted style sheet, there's no need to use the ::shadow
or /deep/
expressions. If you want to refer to the element containing the shadow root, you can use the ::host
pseudo-element.
During the transition phase, style sheets targeting the atom-text-editor
context will also be loaded globally. Make sure you update your selectors in a way that maintains compatibility with the shadow DOM being disabled. That means if you use a ::host
pseudo element, you should also include the same style rule matches against atom-text-editor
.
STOP
This is being kept for archival purposes only from the original Atom documentation. As this may no longer be relevant to Pulsar, you use this at your own risk. Current Pulsar documentation for this section is found at the documentation home.
Now that we've covered the very basics of Atom, we are ready to see how to really start getting the most out of using it. In this chapter we'll look at how to find and install new packages in order to add new functionality, how to find and install new themes, how to work with and manipulate text in a more advanced way, how to customize the editor in any way you wish, how to work with Git for version control and more.
At the end of this chapter, you should have a customized environment that you're comfortable in and you should be able to change, create and move through your text like a master.
First we'll start with the Atom package system. As we mentioned previously, Atom itself is a very basic core of functionality that ships with a number of useful packages that add new features like the Tree View and the Settings View.
In fact, there are more than 80 packages that comprise all of the functionality that is available in Atom by default. For example, the Welcome screen that you see when you first start Atom, the spell checker, the themes and the Fuzzy Finder are all packages that are separately maintained and all use the same APIs that you have access to, as we'll see in great detail in Hacking Atom.
This means that packages can be incredibly powerful and can change everything from the very look and feel of the entire interface to the basic operation of even core functionality.
In order to install a new package, you can use the Install tab in the now familiar Settings View. Open up the Settings View using Cmd+,Ctrl+,, click on the "Install" tab and type your search query into the box under Install Packages.
The packages listed here have been published to https://atom.io/packages which is the official registry for Atom packages. Searching on the Settings View will go to the Atom package registry and pull in anything that matches your search terms.
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Atom, it will show up in the Settings View under the "Packages" tab, along with all the preinstalled packages that come with Atom. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Atom will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Atom from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on atom.io, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
Clicking on "Install" will install the theme and make it available in the Theme dropdowns as we saw in Changing the Theme.
You can also install packages or themes from the command line using apm
.
Tip
Check that you have apm
installed by running the following command in your terminal:
$ apm help install
+
You should see a message print out with details about the apm install
command.
If you do not, see the Installing Atom section for instructions on how to install the atom
and apm
commands for your system.
You can also install packages by using the apm install
command:
apm install <package_name>
to install the latest version.apm install <package_name>@<package_version>
to install a specific version.For example apm install emmet@0.1.5
installs the 0.1.5
release of the Emmet package.
You can also use apm
to find new packages to install. If you run apm search
, you can search the package registry for a search term.
$ apm search coffee
+> Search Results For 'coffee' (29)
+> ├── build-coffee Atom Build provider for coffee, compiles CoffeeScript (1160 downloads, 2 stars)
+> ├── scallahan-coffee-syntax A coffee inspired theme from the guys over at S.CALLAHAN (183 downloads, 0 stars)
+> ├── coffee-paste Copy/Paste As : Js ➤ Coffee / Coffee ➤ Js (902 downloads, 4 stars)
+> ├── atom-coffee-repl Coffee REPL for Atom Editor (894 downloads, 2 stars)
+> ├── coffee-navigator Code navigation panel for Coffee Script (3493 downloads, 22 stars)
+> ...
+> ├── language-iced-coffeescript Iced coffeescript for atom (202 downloads, 1 star)
+> └── slontech-syntax Dark theme for web developers ( HTML, CSS/LESS, PHP, MYSQL, javascript, AJAX, coffee, JSON ) (2018 downloads, 3 stars)
+
You can use apm view
to see more information about a specific package.
$ apm view build-coffee
+> build-coffee
+> ├── 0.6.4
+> ├── https://github.com/idleberg/atom-build-coffee
+> ├── Atom Build provider for coffee, compiles CoffeeScript
+> ├── 1152 downloads
+> └── 2 stars
+>
+> Run `apm install build-coffee` to install this package.
+
While it's pretty easy to move around Atom by clicking with the mouse or using the arrow keys, there are some keybindings that may help you keep your hands on the keyboard and navigate around a little faster.
You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
Atom also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the Command Palette, but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your keymap.cson
to create a key combination. You can open keymap.cson
file in an editor from the Atom > KeymapFile > KeymapEdit > Keymap menu.
For example, the command editor:move-to-beginning-of-screen-line
is available in the command palette, but it's not bound to any key combination. To create a key combination you need to add an entry in your keymap.cson
file. For editor:select-to-previous-word-boundary
, you can add the following to your keymap.cson
:
This will bind the command editor:select-to-previous-word-boundary
to Cmd+Shift+ECtrl+Shift+E. For more information on customizing your keybindings, see Customizing Keybindings.
Here's a list of Movement and Selection Commands that do not have a keyboard shortcut by default:
You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press Cmd+RCtrl+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to Cmd+TCtrl+T. You can also search for symbols across your project but it requires a tags
file.
You can generate a tags
file by using the ctags utility. Once it is installed, you can use it to generate a tags
file by running a command to generate it. See the ctags documentation for details.
You can customize how tags are generated by creating your own .ctags
file in your home directory, ~/.ctags
%USERPROFILE%\.ctags
. An example can be found here.
The symbols navigation functionality is implemented in the symbols-view package.
Atom also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press Cmd+F2Alt+Ctrl+F2Ctrl+Shift+F2, Atom will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Atom will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
![View and filter bookmarks](@images/atom/bookmarks.png "View and filter bookmarks")The bookmarks functionality is implemented in the bookmarks package.
Text selections in Atom support a number of actions, such as scoping deletion, indentation and search actions, and marking text for actions such as quoting and bracketing.
Selections mirror many of the movement commands. They're actually exactly the same keybindings as the movement commands, but with a Shift key added in.
In addition to the cursor movement selection commands, there are also a few commands that help with selecting specific areas of content.
So far we've looked at a number of ways to move around and select regions of a file, so now let's actually change some of that text. Obviously you can type in order to insert characters, but there are also a number of ways to delete and manipulate text that could come in handy.
There are a handful of cool keybindings for basic text manipulation that might come in handy. These range from moving around lines of text and duplicating lines to changing the case.
Atom also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using Alt+Cmd+QAlt+Ctrl+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
One of the cool things that Atom can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.
Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any plugin or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the CmdCtrl key pressed down to select multiple regions of your text simultaneously.
Atom comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
The whitespace commands are implemented in the atom/whitespace package. The settings for the whitespace commands are managed on the page for the whitespace
package.
Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Atom, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Atom will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Atom ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Atom will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, “”
, ‘’
, «»
, ‹›
, and backticks when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Atom will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
The brackets functionality is implemented in the bracket-matcher package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.
Atom also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.
If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.
When you open a file, Atom will try to auto-detect the encoding. If Atom can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.
If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.
The encoding selector is implemented in the encoding-selector package.
Finding and replacing text in your file or project is quick and easy in Atom.
If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press Cmd+FCtrl+F, type in a search string and press Enter (or Cmd+GF3 or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Scott" with the string "Dragon", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
Note
Note: Atom uses JavaScript regular expressions to perform regular expression searches.
When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, … $&. Refer to JavaScript's guide to regular expressions to learn more about regular expression syntax you can use in Atom.
You can also find and replace throughout your entire project if you invoke the panel with Cmd+Shift+FCtrl+Shift+F.
This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.
You can limit a search to a subset of the files in your project by entering a glob pattern into the "File/Directory pattern" text box. For example, the pattern src/*.js
would restrict the search to JavaScript files in the src
directory. The "globstar" pattern (**
) can be used to match arbitrarily many subdirectories. For example, docs/**/*.md
will match docs/a/foo.md
, docs/a/b/foo.md
, etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.
When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders /path1/folder1
and /path2/folder2
open, you could enter a pattern starting with folder1
to search only in the first folder.
Press Esc while focused on the Find and Replace panel to clear the pane from your workspace.
The Find and Replace functionality is implemented in the find-and-replace package and uses the scandal Node module to do the actual searching.
Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Atom, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your ~/.atom
%USERPROFILE%\.atom
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Atom. You can also easily open up that file by selecting the Atom > SnippetsEdit > SnippetsFile > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
Warning
Snippet keys, unlike CSS selectors, can only be repeated once per level. If there are duplicate keys at the same level, then only the last one will be read. See Configuring with CSON for more information.
You can also use CoffeeScript multi-line syntax using """
for larger templates:
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (${1:true}) {
+ $2
+ } else if (${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
💥 just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Atom should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
Again, see Configuring with CSON for more information on CSON key structure and non-repeatability.
The snippets functionality is implemented in the snippets package.
For more examples, see the snippets in the language-html and language-javascript packages.
If you're still looking to save some typing time, Atom also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
The Autocomplete functionality is implemented in the autocomplete-plus package.
If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.
You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the Alt+Cmd+[Alt+Ctrl+[ and Alt+Cmd+]Alt+Ctrl+] keybindings.
To fold everything, use Alt+Cmd+Shift+[Alt+Ctrl+Shift+[ and to unfold everything use Alt+Cmd+Shift+]Alt+Ctrl+Shift+]. You can also fold at a specific indentation level with Cmd+KCtrl+K Cmd+0-9Ctrl+0-9 where the number is the indentation depth.
Finally, you can fold arbitrary sections of your code or text by making a selection and then typing Alt+Cmd+Ctrl+FAlt+Ctrl+F or choosing "Fold Selection" in the Command Palette.
You can split any editor pane horizontally or vertically by using Cmd+KCtrl+K Up/Down/Left/Right where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with Cmd+KCtrl+K Cmd+Up/Down/Left/RightCtrl+Up/Down/Left/Right where the direction is the direction the focus should move to.
Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.
Tip
If you don't like using tabs, you don't have to. You can disable the tabs package and each pane will still support multiple pane items. You just won't have tabs to use to click between them.
To close a pane, you can close all pane items with Cmd+WCtrl+W. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.
"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
The "grammar" of a file is what language Atom has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in Snippets.
When you load a file, Atom does a little work to try to figure out what type of file it is. Largely this is accomplished by looking at its file extension (.md
is generally a Markdown file, etc), though sometimes it has to inspect the content a bit to figure it out.
When you open a file and Atom can't determine a grammar for the file, it will default to "Plain Text", which is the simplest one. If it does default to "Plain Text", picks the wrong grammar for the file, or if for any reason you wish to change the selected grammar, you can pull up the Grammar Selector with Ctrl+Shift+L.
When the grammar of a file is changed, Atom will remember that for the current session.
The Grammar Selector functionality is implemented in the grammar-selector package.
Version control is an important aspect of any project and Atom comes with basic Git and GitHub integration built in.
In order to use version control in Atom, the project root needs to contain the Git repository.
The Alt+Cmd+ZAlt+Ctrl+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use Cmd+ZCtrl+Z afterwards to restore the previous contents.
Atom ships with the fuzzy-finder package which provides Cmd+TCtrl+T to quickly open files in the project and Cmd+BCtrl+B to jump to any open editor. The package also provides Cmd+Shift+BCtrl+Shift+B which displays a list of all the untracked and modified files in the project. These will be the same files that you would see on the command line if you ran git status
.
An icon will appear to the right of each file letting you know whether it is untracked or modified.
Atom can be used as your Git commit editor and ships with the language-git package which adds syntax highlighting to edited commit, merge, and rebase messages.
You can configure Atom to be your Git commit editor with the following command:
$ git config --global core.editor "atom --wait"
+
The language-git package will help remind you to be brief by colorizing the first lines of commit messages when they're longer than 50 or 65 characters.
The status-bar package that ships with Atom includes several Git decorations that display on the right side of the status bar:
The currently checked out branch name is shown with the number of commits the branch is ahead of or behind its upstream branch. An icon is added if the file is untracked, modified, or ignored. The number of lines added and removed since the file was last committed will be displayed as well.
The included git-diff package colorizes the gutter next to lines that have been added, edited, or removed.
This package also adds Alt+G Down and Alt+G Up keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
The github package brings Git and GitHub integration right inside Atom.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Atom and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the Cmd-LeftCtrl-Left or Cmd-RightCtrl-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "👤➕" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Learn more about merge vs. rebase.
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
Though it is probably most common to use Atom to write software code, Atom can also be used to write prose quite effectively. Most often this is done in some sort of markup language such as Asciidoc or Markdown (in which this manual is written). Here we'll quickly cover a few of the tools Atom provides for helping you write prose.
In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Atom will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting Cmd+Shift+;Ctrl+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Atom will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
The spell checking is implemented in the spell-check package.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Atom ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press Cmd+CCtrl+CCtrl+Ins or if you right-click in the preview pane and choose "Copy as HTML".
Markdown preview is implemented in the markdown-preview package.
There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing "Snippets: Available" in the Command Palette.
Now that we are feeling comfortable with just about everything built into Atom, let's look at how to tweak it. Perhaps there is a keybinding that you use a lot but feels wrong or a color that isn't quite right for you. Atom is amazingly flexible, so let's go over some of the simpler flexes it can do.
All of Atom's config files (with the exception of your style sheet and your Init Script) are written in CSON, short for CoffeeScript Object Notation. Just like its namesake JSON, JavaScript Object Notation, CSON is a text format for storing structured data in the form of simple objects made up of key-value pairs.
key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Atom's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your ~/.atom
%USERPROFILE%\.atom
directory. You can open this file in an editor from the Atom > StylesheetFile > StylesheetEdit > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing Alt+Cmd+ICtrl+Shift+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Atom. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
Tip
If you are unfamiliar with Less, it is a basic CSS preprocessor that makes some things in CSS a bit easier. You can learn more about it at lesscss.org.
If you prefer to use CSS instead, you can do that in the same styles.less
file, since CSS is also valid in Less.
Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Atom keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Atom's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Atom is started. It will always be loaded last, giving you the chance to override bindings that are defined by Atom's core keymaps or third-party packages. You can open this file in an editor from the Atom > KeymapFile > KeymapEdit > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Atom in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the Cmd+.Ctrl+. key combination. It will show you what keys Atom saw you press and what command Atom executed because of that combination.
Atom loads configuration settings from the config.cson
file in your ~/.atom
%USERPROFILE%\.atom
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the Atom > ConfigFile > ConfigEdit > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of AtomprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Atom to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press Cmd+Shift+PCtrl+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and press Alt+Cmd+Pchoose "Editor: Log Cursor Scope" in the Command Palette to show all scopes for the current position of the cursor. The scope mentioned top most is always the language for this kind of file, the scopes following are specific to the cursor position:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Atom to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Atom are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Atom Home directory will, by default, be called .atom
and will be located in the root of the home directory of the user.
An environment variable can be used to make Atom use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Atom Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Atom settings.
In addition to using the ATOM_HOME
environment variable, Atom can also be set to use "Portable Mode".
Portable Mode is most useful for taking Atom with you, with all your custom setting and packages, from machine to machine. This may take the form of keeping Atom on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Atom is in Portable Mode when there is a directory named .atom sibling to the directory in which the atom executable file lives. For example, the installed Atom directory can be placed into a Dropbox folder next to a .atom folder.
With such a setup, Atom will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Atom provides a command-line parameter option for setting Portable Mode.
$ atom --portable
+
Executing atom with the --portable
option will take the .atom
directory you have in the default location (~/.atom
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
At this point you should be something of an Atom master user. You should be able to navigate and manipulate your text and files like a wizard. You should also be able to customize Atom backwards and forwards to make it look and act just how you want it to.
In the next chapter, we're going to kick it up a notch: we'll take a look at changing and adding new functionality to the core of Atom itself. We're going to start creating packages for Atom. If you can dream it, you can build it.
First we'll start with the Atom package system. As we mentioned previously, Atom itself is a very basic core of functionality that ships with a number of useful packages that add new features like the Tree View and the Settings View.
In fact, there are more than 80 packages that comprise all of the functionality that is available in Atom by default. For example, the Welcome screen that you see when you first start Atom, the spell checker, the themes and the Fuzzy Finder are all packages that are separately maintained and all use the same APIs that you have access to, as we'll see in great detail in Hacking Atom.
This means that packages can be incredibly powerful and can change everything from the very look and feel of the entire interface to the basic operation of even core functionality.
In order to install a new package, you can use the Install tab in the now familiar Settings View. Open up the Settings View using Cmd+,Ctrl+,, click on the "Install" tab and type your search query into the box under Install Packages.
The packages listed here have been published to https://atom.io/packages which is the official registry for Atom packages. Searching on the Settings View will go to the Atom package registry and pull in anything that matches your search terms.
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Atom, it will show up in the Settings View under the "Packages" tab, along with all the preinstalled packages that come with Atom. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Atom will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Atom from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on atom.io, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
Clicking on "Install" will install the theme and make it available in the Theme dropdowns as we saw in Changing the Theme.
You can also install packages or themes from the command line using apm
.
Tip
Check that you have apm
installed by running the following command in your terminal:
$ apm help install
+
You should see a message print out with details about the apm install
command.
If you do not, see the Installing Atom section for instructions on how to install the atom
and apm
commands for your system.
You can also install packages by using the apm install
command:
apm install <package_name>
to install the latest version.apm install <package_name>@<package_version>
to install a specific version.For example apm install emmet@0.1.5
installs the 0.1.5
release of the Emmet package.
You can also use apm
to find new packages to install. If you run apm search
, you can search the package registry for a search term.
$ apm search coffee
+> Search Results For 'coffee' (29)
+> ├── build-coffee Atom Build provider for coffee, compiles CoffeeScript (1160 downloads, 2 stars)
+> ├── scallahan-coffee-syntax A coffee inspired theme from the guys over at S.CALLAHAN (183 downloads, 0 stars)
+> ├── coffee-paste Copy/Paste As : Js ➤ Coffee / Coffee ➤ Js (902 downloads, 4 stars)
+> ├── atom-coffee-repl Coffee REPL for Atom Editor (894 downloads, 2 stars)
+> ├── coffee-navigator Code navigation panel for Coffee Script (3493 downloads, 22 stars)
+> ...
+> ├── language-iced-coffeescript Iced coffeescript for atom (202 downloads, 1 star)
+> └── slontech-syntax Dark theme for web developers ( HTML, CSS/LESS, PHP, MYSQL, javascript, AJAX, coffee, JSON ) (2018 downloads, 3 stars)
+
You can use apm view
to see more information about a specific package.
$ apm view build-coffee
+> build-coffee
+> ├── 0.6.4
+> ├── https://github.com/idleberg/atom-build-coffee
+> ├── Atom Build provider for coffee, compiles CoffeeScript
+> ├── 1152 downloads
+> └── 2 stars
+>
+> Run `apm install build-coffee` to install this package.
+
Text selections in Atom support a number of actions, such as scoping deletion, indentation and search actions, and marking text for actions such as quoting and bracketing.
Selections mirror many of the movement commands. They're actually exactly the same keybindings as the movement commands, but with a Shift key added in.
In addition to the cursor movement selection commands, there are also a few commands that help with selecting specific areas of content.
If you're still looking to save some typing time, Atom also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
The Autocomplete functionality is implemented in the autocomplete-plus package.
Now that we are feeling comfortable with just about everything built into Atom, let's look at how to tweak it. Perhaps there is a keybinding that you use a lot but feels wrong or a color that isn't quite right for you. Atom is amazingly flexible, so let's go over some of the simpler flexes it can do.
All of Atom's config files (with the exception of your style sheet and your Init Script) are written in CSON, short for CoffeeScript Object Notation. Just like its namesake JSON, JavaScript Object Notation, CSON is a text format for storing structured data in the form of simple objects made up of key-value pairs.
key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Atom's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your ~/.atom
%USERPROFILE%\.atom
directory. You can open this file in an editor from the Atom > StylesheetFile > StylesheetEdit > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing Alt+Cmd+ICtrl+Shift+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Atom. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
Tip
If you are unfamiliar with Less, it is a basic CSS preprocessor that makes some things in CSS a bit easier. You can learn more about it at lesscss.org.
If you prefer to use CSS instead, you can do that in the same styles.less
file, since CSS is also valid in Less.
Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Atom keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Atom's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Atom is started. It will always be loaded last, giving you the chance to override bindings that are defined by Atom's core keymaps or third-party packages. You can open this file in an editor from the Atom > KeymapFile > KeymapEdit > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Atom in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the Cmd+.Ctrl+. key combination. It will show you what keys Atom saw you press and what command Atom executed because of that combination.
Atom loads configuration settings from the config.cson
file in your ~/.atom
%USERPROFILE%\.atom
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the Atom > ConfigFile > ConfigEdit > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of AtomprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Atom will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Atom to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press Cmd+Shift+PCtrl+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and press Alt+Cmd+Pchoose "Editor: Log Cursor Scope" in the Command Palette to show all scopes for the current position of the cursor. The scope mentioned top most is always the language for this kind of file, the scopes following are specific to the cursor position:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Atom to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Atom are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Atom Home directory will, by default, be called .atom
and will be located in the root of the home directory of the user.
An environment variable can be used to make Atom use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Atom Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Atom settings.
In addition to using the ATOM_HOME
environment variable, Atom can also be set to use "Portable Mode".
Portable Mode is most useful for taking Atom with you, with all your custom setting and packages, from machine to machine. This may take the form of keeping Atom on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Atom is in Portable Mode when there is a directory named .atom sibling to the directory in which the atom executable file lives. For example, the installed Atom directory can be placed into a Dropbox folder next to a .atom folder.
With such a setup, Atom will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Atom provides a command-line parameter option for setting Portable Mode.
$ atom --portable
+
Executing atom with the --portable
option will take the .atom
directory you have in the default location (~/.atom
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
So far we've looked at a number of ways to move around and select regions of a file, so now let's actually change some of that text. Obviously you can type in order to insert characters, but there are also a number of ways to delete and manipulate text that could come in handy.
There are a handful of cool keybindings for basic text manipulation that might come in handy. These range from moving around lines of text and duplicating lines to changing the case.
Atom also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using Alt+Cmd+QAlt+Ctrl+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
One of the cool things that Atom can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.
Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any plugin or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the CmdCtrl key pressed down to select multiple regions of your text simultaneously.
Atom comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
The whitespace commands are implemented in the atom/whitespace package. The settings for the whitespace commands are managed on the page for the whitespace
package.
Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Atom, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Atom will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Atom ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Atom will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, “”
, ‘’
, «»
, ‹›
, and backticks when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Atom will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
The brackets functionality is implemented in the bracket-matcher package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.
Atom also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.
If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.
When you open a file, Atom will try to auto-detect the encoding. If Atom can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.
If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.
The encoding selector is implemented in the encoding-selector package.
Finding and replacing text in your file or project is quick and easy in Atom.
If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press Cmd+FCtrl+F, type in a search string and press Enter (or Cmd+GF3 or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Scott" with the string "Dragon", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
Note
Note: Atom uses JavaScript regular expressions to perform regular expression searches.
When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, … $&. Refer to JavaScript's guide to regular expressions to learn more about regular expression syntax you can use in Atom.
You can also find and replace throughout your entire project if you invoke the panel with Cmd+Shift+FCtrl+Shift+F.
This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.
You can limit a search to a subset of the files in your project by entering a glob pattern into the "File/Directory pattern" text box. For example, the pattern src/*.js
would restrict the search to JavaScript files in the src
directory. The "globstar" pattern (**
) can be used to match arbitrarily many subdirectories. For example, docs/**/*.md
will match docs/a/foo.md
, docs/a/b/foo.md
, etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.
When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders /path1/folder1
and /path2/folder2
open, you could enter a pattern starting with folder1
to search only in the first folder.
Press Esc while focused on the Find and Replace panel to clear the pane from your workspace.
The Find and Replace functionality is implemented in the find-and-replace package and uses the scandal Node module to do the actual searching.
If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.
You can fold blocks of code by clicking the arrows that appear when you hover your mouse cursor over the gutter. You can also fold and unfold from the keyboard with the Alt+Cmd+[Alt+Ctrl+[ and Alt+Cmd+]Alt+Ctrl+] keybindings.
To fold everything, use Alt+Cmd+Shift+[Alt+Ctrl+Shift+[ and to unfold everything use Alt+Cmd+Shift+]Alt+Ctrl+Shift+]. You can also fold at a specific indentation level with Cmd+KCtrl+K Cmd+0-9Ctrl+0-9 where the number is the indentation depth.
Finally, you can fold arbitrary sections of your code or text by making a selection and then typing Alt+Cmd+Ctrl+FAlt+Ctrl+F or choosing "Fold Selection" in the Command Palette.
The github package brings Git and GitHub integration right inside Atom.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Atom and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the Cmd-LeftCtrl-Left or Cmd-RightCtrl-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "👤➕" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Learn more about merge vs. rebase.
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
The "grammar" of a file is what language Atom has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in Snippets.
When you load a file, Atom does a little work to try to figure out what type of file it is. Largely this is accomplished by looking at its file extension (.md
is generally a Markdown file, etc), though sometimes it has to inspect the content a bit to figure it out.
When you open a file and Atom can't determine a grammar for the file, it will default to "Plain Text", which is the simplest one. If it does default to "Plain Text", picks the wrong grammar for the file, or if for any reason you wish to change the selected grammar, you can pull up the Grammar Selector with Ctrl+Shift+L.
When the grammar of a file is changed, Atom will remember that for the current session.
The Grammar Selector functionality is implemented in the grammar-selector package.
While it's pretty easy to move around Atom by clicking with the mouse or using the arrow keys, there are some keybindings that may help you keep your hands on the keyboard and navigate around a little faster.
You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
Atom also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the Command Palette, but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your keymap.cson
to create a key combination. You can open keymap.cson
file in an editor from the Atom > KeymapFile > KeymapEdit > Keymap menu.
For example, the command editor:move-to-beginning-of-screen-line
is available in the command palette, but it's not bound to any key combination. To create a key combination you need to add an entry in your keymap.cson
file. For editor:select-to-previous-word-boundary
, you can add the following to your keymap.cson
:
This will bind the command editor:select-to-previous-word-boundary
to Cmd+Shift+ECtrl+Shift+E. For more information on customizing your keybindings, see Customizing Keybindings.
Here's a list of Movement and Selection Commands that do not have a keyboard shortcut by default:
You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press Cmd+RCtrl+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to Cmd+TCtrl+T. You can also search for symbols across your project but it requires a tags
file.
You can generate a tags
file by using the ctags utility. Once it is installed, you can use it to generate a tags
file by running a command to generate it. See the ctags documentation for details.
You can customize how tags are generated by creating your own .ctags
file in your home directory, ~/.ctags
%USERPROFILE%\.ctags
. An example can be found here.
The symbols navigation functionality is implemented in the symbols-view package.
Atom also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press Cmd+F2Alt+Ctrl+F2Ctrl+Shift+F2, Atom will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Atom will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
![View and filter bookmarks](@images/atom/bookmarks.png "View and filter bookmarks")The bookmarks functionality is implemented in the bookmarks package.
You can split any editor pane horizontally or vertically by using Cmd+KCtrl+K Up/Down/Left/Right where the direction key is the direction to split the pane. Once you have a split pane, you can switch between them with Cmd+KCtrl+K Cmd+Up/Down/Left/RightCtrl+Up/Down/Left/Right where the direction is the direction the focus should move to.
Each pane has its own "pane items", which are represented by tabs. You can move the files from pane to pane by dragging them with the mouse and dropping them in the pane you want that file to be in.
Tip
If you don't like using tabs, you don't have to. You can disable the tabs package and each pane will still support multiple pane items. You just won't have tabs to use to click between them.
To close a pane, you can close all pane items with Cmd+WCtrl+W. You can configure whether panes auto-close when empty in the Settings View with the "Remove Empty Panes" setting under Core Settings.
"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Atom, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your ~/.atom
%USERPROFILE%\.atom
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Atom. You can also easily open up that file by selecting the Atom > SnippetsEdit > SnippetsFile > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
Warning
Snippet keys, unlike CSS selectors, can only be repeated once per level. If there are duplicate keys at the same level, then only the last one will be read. See Configuring with CSON for more information.
You can also use CoffeeScript multi-line syntax using """
for larger templates:
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (${1:true}) {
+ $2
+ } else if (${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
💥 just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Atom should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
Again, see Configuring with CSON for more information on CSON key structure and non-repeatability.
The snippets functionality is implemented in the snippets package.
For more examples, see the snippets in the language-html and language-javascript packages.
At this point you should be something of an Atom master user. You should be able to navigate and manipulate your text and files like a wizard. You should also be able to customize Atom backwards and forwards to make it look and act just how you want it to.
In the next chapter, we're going to kick it up a notch: we'll take a look at changing and adding new functionality to the core of Atom itself. We're going to start creating packages for Atom. If you can dream it, you can build it.
Version control is an important aspect of any project and Atom comes with basic Git and GitHub integration built in.
In order to use version control in Atom, the project root needs to contain the Git repository.
The Alt+Cmd+ZAlt+Ctrl+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use Cmd+ZCtrl+Z afterwards to restore the previous contents.
Atom ships with the fuzzy-finder package which provides Cmd+TCtrl+T to quickly open files in the project and Cmd+BCtrl+B to jump to any open editor. The package also provides Cmd+Shift+BCtrl+Shift+B which displays a list of all the untracked and modified files in the project. These will be the same files that you would see on the command line if you ran git status
.
An icon will appear to the right of each file letting you know whether it is untracked or modified.
Atom can be used as your Git commit editor and ships with the language-git package which adds syntax highlighting to edited commit, merge, and rebase messages.
You can configure Atom to be your Git commit editor with the following command:
$ git config --global core.editor "atom --wait"
+
The language-git package will help remind you to be brief by colorizing the first lines of commit messages when they're longer than 50 or 65 characters.
The status-bar package that ships with Atom includes several Git decorations that display on the right side of the status bar:
The currently checked out branch name is shown with the number of commits the branch is ahead of or behind its upstream branch. An icon is added if the file is untracked, modified, or ignored. The number of lines added and removed since the file was last committed will be displayed as well.
The included git-diff package colorizes the gutter next to lines that have been added, edited, or removed.
This package also adds Alt+G Down and Alt+G Up keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
Though it is probably most common to use Atom to write software code, Atom can also be used to write prose quite effectively. Most often this is done in some sort of markup language such as Asciidoc or Markdown (in which this manual is written). Here we'll quickly cover a few of the tools Atom provides for helping you write prose.
In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Atom will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting Cmd+Shift+;Ctrl+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Atom will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
The spell checking is implemented in the spell-check package.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Atom ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press Cmd+CCtrl+CCtrl+Ins or if you right-click in the preview pane and choose "Copy as HTML".
Markdown preview is implemented in the markdown-preview package.
There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing "Snippets: Available" in the Command Palette.
Here you will find everything you need to know about Pulsar, from how to install it all the way to hacking away at its core to make it your own.
The documentation has been broken down into various sections which you can find below.
The Launch Manual contains everything you need to know about Pulsar. It covers topics all the way from the initial installation through to developing and publishing your own packages.
This section is dedicated to info and documentation around Pulsar's numerous packages which make up both the core editor and community ecosystem.
There are a number of miscellaneous resources here that do not necessarily fit into other topics. This is where you will find information about things like the tools used by the team and contributors, a glossary to translate any confusing jargon and details of the various APIs that make up Pulsar.
This is an archive of the old Atom documentation as it appeared on their Flight Manual.
Anything here is just a historical reference of our past, when Atom existed as a GitHub supported project.
This includes their original layout and formatting of how things were expected to appear. Anything listed here, may work, and it may not. This is taking into consideration their sunset and our picking up of their torch.
Here you will learn everything you need to know about actually using Pulsar. This introduces what Pulsar is, how to install it and how to use its basic features.
This section is all about the various functions built right into Pulsar and how you can use them to get the most out of it.
It will teach you everything from cursor movement all the way to basic customization of the application.
This is really what Pulsar is all about, here you will get to grips with what lies under the skin of Pulsar so you can begin hacking on it to extend and customize its functionality.
Here you will get into the real heart of Pulsar, where you get into the details of various internal APIs and full details of how to maintain packages.
Frequently asked questions. This is where you can find some of the more common questions and answers regarding Pulsar.
Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
Now that we've written a number of packages and themes, let's take minute to take a closer look at some of the ways that Pulsar works in greater depth. Here we'll go into more of a deep dive on individual internal APIs and systems of Pulsar, even looking at some Atom source to see how things are really getting done.
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with `config.get`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
Subscription methods return Disposable
objects that can be used to unsubscribe. Note in the example above how we save the subscription to the @fontSizeObserveSubscription
instance variable and dispose of it when the view is detached. To group multiple subscriptions together, you can add them all to a CompositeDisposable
that you dispose when the view is detached.
The atom.config
database is populated on startup from LNX/MAC: ~/.pulsar/config.cson
- WIN: %USERPROFILE%\.pulsar\config.cson
but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
If you're exposing package configuration via specific key paths, you'll want to associate them with a schema in your package's main module. Read more about schemas in the Config API documentation.
Keymap files are encoded as JSON or CSON files containing nested hashes. They work much like style sheets, but instead of applying style properties to elements matching the selector, they specify the meaning of keystrokes on elements matching the selector. Here is an example of some bindings that apply when keystrokes pass through atom-text-editor
elements:
Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and LNX/WIN: Ctrl+Backspace - MAC: Alt+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Pulsar, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.js
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for LNX/WIN: Ctrl+O - MAC: Cmd+O when the selection is inside an editor pane:
But if you click inside the Tree View and press LNX/WIN: Ctrl+O - MAC: Cmd+O , it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.Sometimes the problem isn't mapping the command to a key combination, the problem is that Pulsar doesn't recognize properly what keys you're pressing. This is due to some limitations in how Chromium reports keyboard events. But even this can be customized now.
You can add the following to your init.js
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
Or if you are still using the init.coffee
file:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
If you want to know the event
for the keystroke you pressed you can paste the following script to your developer tools console
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Pulsar to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
Pulsar supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
Config::set
accepts a scopeSelector
. If you'd like to set a setting for JavaScript function names, you can give it the JavaScript function name scopeSelector
:
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
A scope descriptor is an Object that wraps an Array
of String
s. The Array describes a path from the root of the syntax tree to a token including all scope names for the entire path.
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
Config::get
accepts a scopeDescriptor
. You can get the value for your setting scoped to JavaScript function names via:
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
Editor::getRootScopeDescriptor
to get the language's descriptor. For example: [".source.js"]
Editor::scopeDescriptorForBufferPosition
to get the descriptor at a specific position in the buffer.Cursor::getScopeDescriptor
to get a cursor's descriptor based on position. eg. if the cursor were in the name of the method in our example it would return ["source.js", "meta.function.js", "entity.name.function.js"]
Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer—the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Pulsar to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Pulsar will only attempt to call deserialize if the two versions match, and otherwise return undefined.
Pulsar contains a number of packages that are Node modules instead of Pulsar packages. If you want to make changes to the Node modules, for instance atom-keymap
, you have to link them into the development environment differently than you would a normal Pulsar package.
Here are the steps to run a local version of a Node module within Pulsar. We're using atom-keymap
as an example:
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Pulsar and do the following:
$ cd <WHERE YOU CLONED THE NODE MODULE>
+$ npm install
+$ cd <WHERE YOU CLONED PULSAR>
+$ pulsar -p rebuild
+$ pulsar --dev .
+
Pulsar packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
Similarly, to consume a service, specify one or more version ranges, each paired with the name of a method on the package's main module:
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
Pre-release information
This section is about a feature in pre-release. The information below documents the intended functionality but there is still ongoing work to support these features with stability.
While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
STOP
Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the ppm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if ppm has taken care of things for you.
Note
Note: The ppm tool will only publish and https://pulsar-edit.dev will only list packages that are hosted on GitHub, regardless of what process is used to publish them.
When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\d+\.\d+\.\d+
^v\d+\.\d+\.\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
pulsar -p publish --tag tagname
where tagname
must match the name of the tag created in the above stepSome packages get too big for one person. Sometimes priorities change and someone else wants to help out. You can let others help or create co-owners by adding them as a collaborator on the GitHub repository for your package. Note: Anyone that has push access to your repository will have the ability to publish new versions of the package that belongs to that repository.
You can also have packages that are owned by a GitHub organization. Anyone who is a member of an organization's team which has push access to the package's repository will be able to publish new versions of the package.
STOP
🚨 This is a permanent change. There is no going back! 🚨
If you want to hand off support of your package to someone else, you can do that by transferring the package's repository to the new owner. Once you do that, they can publish a new version with the updated repository information in the package.json
.
If you no longer want to support your package and cannot find anyone to take it over, you can unpublish your package from https://pulsar-edit.dev. For example, if your package is named package-name
then the command you would execute is:
$ pulsar -p unpublish <package-name>
+
This will remove your package from the https://pulsar-edit.dev package registry. Anyone who has already downloaded a copy of your package will still have it and be able to use it, but it will no longer be available for installation by others.
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ pulsar -p unpublish <package-name@1.2.3>
+
This will remove just this particular version from the https://pulsar-edit.dev package registry.
If you need to rename your package for any reason, you can do so with one simple command – pulsar -p publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ pulsar -p publish --rename <new-package-name>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
You should now have a better understanding of some of the core Pulsar APIs and systems.
If you are writing a package that you want to make configurable, you'll need to read config settings via the atom.config
global. You can read the current value of a namespaced config key with atom.config.get
:
// read a value with `config.get`
+if (atom.config.get("editor.showInvisibles")) {
+ this.showInvisibles();
+}
+
Or you can subscribe via atom.config.observe
to track changes from any view object.
const {View} = require('space-pen')
+
+class MyView extends View {
+ function attached() {
+ this.fontSizeObserveSubscription =
+ atom.config.observe('editor.fontSize', (newValue, {previous}) => {
+ this.adjustFontSize(newValue)
+ })
+ }
+
+ function detached() {
+ this.fontSizeObserveSubscription.dispose()
+ }
+}
+
The atom.config.observe
method will call the given callback immediately with the current value for the specified key path, and it will also call it in the future whenever the value of that key path changes. If you only want to invoke the callback the next time the value changes, use atom.config.onDidChange
instead.
Subscription methods return Disposable
objects that can be used to unsubscribe. Note in the example above how we save the subscription to the @fontSizeObserveSubscription
instance variable and dispose of it when the view is detached. To group multiple subscriptions together, you can add them all to a CompositeDisposable
that you dispose when the view is detached.
The atom.config
database is populated on startup from LNX/MAC: ~/.pulsar/config.cson
- WIN: %USERPROFILE%\.pulsar\config.cson
but you can programmatically write to it with atom.config.set
:
// basic key update
+atom.config.set("core.showInvisibles", true);
+
If you're exposing package configuration via specific key paths, you'll want to associate them with a schema in your package's main module. Read more about schemas in the Config API documentation.
Pulsar contains a number of packages that are Node modules instead of Pulsar packages. If you want to make changes to the Node modules, for instance atom-keymap
, you have to link them into the development environment differently than you would a normal Pulsar package.
Here are the steps to run a local version of a Node module within Pulsar. We're using atom-keymap
as an example:
After you get the Node module linked and working, every time you make a change to the Node module's code, you will have to exit Pulsar and do the following:
$ cd <WHERE YOU CLONED THE NODE MODULE>
+$ npm install
+$ cd <WHERE YOU CLONED PULSAR>
+$ pulsar -p rebuild
+$ pulsar --dev .
+
Pulsar packages can interact with each other through versioned APIs called services. To provide a service, in your package.json
, specify one or more version numbers, each paired with the name of a method on your package's main module:
{
+ "providedServices": {
+ "my-service": {
+ "description": "Does a useful thing",
+ "versions": {
+ "1.2.3": "provideMyServiceV1",
+ "2.3.4": "provideMyServiceV2"
+ }
+ }
+ }
+}
+
In your package's main module, implement the methods named above. These methods will be called any time a package is activated that consumes their corresponding service. They should return a value that implements the service's API.
module.exports = {
+ activate() {
+ // ...
+ },
+
+ provideMyServiceV1() {
+ return adaptToLegacyAPI(myService);
+ },
+
+ provideMyServiceV2() {
+ return myService;
+ },
+};
+
Similarly, to consume a service, specify one or more version ranges, each paired with the name of a method on the package's main module:
{
+ "consumedServices": {
+ "another-service": {
+ "versions": {
+ "^1.2.3": "consumeAnotherServiceV1",
+ ">=2.3.4 <2.5": "consumeAnotherServiceV2"
+ }
+ }
+ }
+}
+
These methods will be called any time a package is activated that provides their corresponding service. They will receive the service object as an argument. You will usually need to perform some kind of cleanup in the event that the package providing the service is deactivated. To do this, return a Disposable
from your service-consuming method:
const { Disposable } = require("atom");
+
+module.exports = {
+ activate() {
+ // ...
+ },
+
+ consumeAnotherServiceV1(service) {
+ useService(adaptServiceFromLegacyAPI(service));
+ return new Disposable(() => stopUsingService(service));
+ },
+
+ consumeAnotherServiceV2(service) {
+ useService(service);
+ return new Disposable(() => stopUsingService(service));
+ },
+};
+
Keymap files are encoded as JSON or CSON files containing nested hashes. They work much like style sheets, but instead of applying style properties to elements matching the selector, they specify the meaning of keystrokes on elements matching the selector. Here is an example of some bindings that apply when keystrokes pass through atom-text-editor
elements:
Beneath the first selector are several keybindings, mapping specific key combinations to commands. When an element with the atom-text-editor
class is focused and LNX/WIN: Ctrl+Backspace - MAC: Alt+Backspace is pressed, a custom DOM event called editor:delete-to-beginning-of-word
is emitted on the atom-text-editor
element.
The second selector group also targets editors, but only if they don't have the mini
attribute. In this example, the commands for code folding don't really make sense on mini-editors, so the selector restricts them to regular editors.
Key combinations express one or more keys combined with optional modifier keys. For example: ctrl-w v
, or cmd-shift-up
. A key combination is composed of the following symbols, separated by a -
. A key sequence can be expressed as key combinations separated by spaces.
Type | Examples |
---|---|
Character literals | a 4 $ |
Modifier keys | cmd ctrl alt shift |
Special keys | enter escape backspace delete tab home end pageup pagedown left right up down space |
Commands are custom DOM events that are triggered when a key combination or sequence matches a binding. This allows user interface code to listen for named commands without specifying the specific keybinding that triggers it. For example, the following code creates a command to insert the current date in an editor:
atom.commands.add("atom-text-editor", {
+ "user:insert-date": function (event) {
+ const editor = this.getModel();
+ return editor.insertText(new Date().toLocaleString());
+ },
+});
+
atom.commands
refers to the global CommandRegistry
instance where all commands are set and consequently picked up by the command palette.
When you are looking to bind new keys, it is often useful to use the Command Palette (LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P) to discover what commands are being listened for in a given focus context. Commands are "humanized" following a simple algorithm, so a command like editor:fold-current-row
would appear as "Editor: Fold Current Row".
A common question is, "How do I make a single keybinding execute two or more commands?" There isn't any direct support for this in Pulsar, but it can be achieved by creating a custom command that performs the multiple actions you desire and then creating a keybinding for that command. For example, let's say I want to create a "composed" command that performs a Select Line followed by Cut. You could add the following to your init.js
:
atom.commands.add("atom-text-editor", "custom:cut-line", function () {
+ const editor = this.getModel();
+ editor.selectLinesContainingCursors();
+ editor.cutSelectedText();
+});
+
Then let's say we want to map this custom command to alt-ctrl-z
, you could add the following to your keymap:
'atom-text-editor':
+ 'alt-ctrl-z': 'custom:cut-line'
+
As is the case with CSS applying styles, when multiple bindings match for a single element, the conflict is resolved by choosing the most specific selector. If two matching selectors have the same specificity, the binding for the selector appearing later in the cascade takes precedence.
Currently, there's no way to specify selector ordering within a single keymap, because JSON objects do not preserve order. We handle cases where selector ordering is critical by breaking the keymap into separate files, such as snippets-1.cson
and snippets-2.cson
.
If a keybinding should only apply to a specific grammar, you can limit bindings to that grammar using the data-grammar
attribute on the atom-text-editor
element:
"atom-text-editor[data-grammar='source example']":
+ 'ctrl-.': 'custom:custom-command'
+
While selectors can be applied to the entire editor by what grammar is associated with it, they cannot be applied to scopes defined within the grammar or to sub-elements of atom-text-editor
.
When the keymap system encounters a binding with the unset!
directive as its command, it will treat the current element as if it had no key bindings matching the current keystroke sequence and continue searching from its parent. For example, the following code removes the keybinding for a
in the Tree View, which is normally used to trigger the tree-view:add-file
command:
'.tree-view':
+ 'a': 'unset!'
+
But if some element above the Tree View had a keybinding for a
, that keybinding would still execute even when the focus is inside the Tree View.
When the keymap system encounters a binding with the abort!
directive as its command, it will stop searching for a keybinding. For example, the following code removes the keybinding for LNX/WIN: Ctrl+O - MAC: Cmd+O when the selection is inside an editor pane:
But if you click inside the Tree View and press LNX/WIN: Ctrl+O - MAC: Cmd+O , it will work.
If you want to force the native browser behavior for a given keystroke, use the native!
directive as the command of a binding. This can be useful to enable the correct behavior in native input elements. If you apply the .native-key-bindings
class to an element, all the keystrokes typically handled by the browser will be assigned the native!
directive.
Tips
Tip: Components and input elements may not correctly handle backspace and arrow keys without forcing this behavior. If your backspace isn't working correctly inside of a component, add either the directive or the .native-key-bindings
class.
Occasionally, it makes sense to layer multiple actions on top of the same key binding. An example of this is the snippets package. Snippets are inserted by typing a snippet prefix such as for
and then pressing Tab. Every time Tab is pressed, we want to execute code attempting to expand a snippet if one exists for the text preceding the cursor. If a snippet doesn't exist, we want Tab to actually insert whitespace.
To achieve this, the snippets package makes use of the .abortKeyBinding()
method on the event object representing the snippets:expand
command.
// pseudo-code
+editor.command("snippets:expand", (e) => {
+ if (this.cursorFollowsValidPrefix()) {
+ this.expandSnippet();
+ } else {
+ e.abortKeyBinding();
+ }
+});
+
When the event handler observes that the cursor does not follow a valid prefix, it calls e.abortKeyBinding()
, telling the keymap system to continue searching for another matching binding.
.abortKeyBinding()
is called on the triggered event object, the search is resumed, triggering a binding on the next-most-specific CSS selector for the same element or continuing upward to parent elements.Sometimes the problem isn't mapping the command to a key combination, the problem is that Pulsar doesn't recognize properly what keys you're pressing. This is due to some limitations in how Chromium reports keyboard events. But even this can be customized now.
You can add the following to your init.js
to send Ctrl+@ when you press Ctrl+Alt+G:
atom.keymaps.addKeystrokeResolver(({ event }) => {
+ if (
+ event.code === "KeyG" &&
+ event.altKey &&
+ event.ctrlKey &&
+ event.type !== "keyup"
+ ) {
+ return "ctrl-@";
+ }
+});
+
Or if you are still using the init.coffee
file:
atom.keymaps.addKeystrokeResolver ({event}) ->
+ if event.code is 'KeyG' and event.altKey and event.ctrlKey and event.type isnt 'keyup'
+ return 'ctrl-@'
+
If you want to know the event
for the keystroke you pressed you can paste the following script to your developer tools console
document.addEventListener("keydown", (e) => console.log(e), true);
+
This will print every keypress event in Pulsar to the console so you can inspect KeyboardEvent.key
and KeyboardEvent.code
.
Pre-release information
This section is about a feature in pre-release. The information below documents the intended functionality but there is still ongoing work to support these features with stability.
While publishing is, by far, the most common action you will perform when working with the packages you provide, there are other things you may need to do.
STOP
Publishing a package manually is not a recommended practice and is only for the advanced user who has published packages before. If you perform the steps wrong, you may be unable to publish the new version of your package and may have to completely unpublish your package in order to correct the faulty state. You have been warned.
Some people prefer to control every aspect of the package publishing process. Normally, the ppm tool manages certain details during publishing to keep things consistent and make everything work smoothly. If you're one of those people that prefers to do things manually, there are certain steps you'll have to take in order to make things work just as smoothly as if ppm has taken care of things for you.
Note
Note: The ppm tool will only publish and https://pulsar-edit.dev will only list packages that are hosted on GitHub, regardless of what process is used to publish them.
When you have completed the changes that you want to publish and are ready to start the publishing process, you must perform the following steps on the master
branch:
package.json
. The version number must match the regular expression: ^\d+\.\d+\.\d+
^v\d+\.\d+\.\d+
and the part after the v
must match the full text of the version number in the package.json
git push --follow-tags
pulsar -p publish --tag tagname
where tagname
must match the name of the tag created in the above stepSome packages get too big for one person. Sometimes priorities change and someone else wants to help out. You can let others help or create co-owners by adding them as a collaborator on the GitHub repository for your package. Note: Anyone that has push access to your repository will have the ability to publish new versions of the package that belongs to that repository.
You can also have packages that are owned by a GitHub organization. Anyone who is a member of an organization's team which has push access to the package's repository will be able to publish new versions of the package.
STOP
🚨 This is a permanent change. There is no going back! 🚨
If you want to hand off support of your package to someone else, you can do that by transferring the package's repository to the new owner. Once you do that, they can publish a new version with the updated repository information in the package.json
.
If you no longer want to support your package and cannot find anyone to take it over, you can unpublish your package from https://pulsar-edit.dev. For example, if your package is named package-name
then the command you would execute is:
$ pulsar -p unpublish <package-name>
+
This will remove your package from the https://pulsar-edit.dev package registry. Anyone who has already downloaded a copy of your package will still have it and be able to use it, but it will no longer be available for installation by others.
If you mistakenly published a version of your package or perhaps you find a glaring bug or security hole, you may want to unpublish just that version of your package. For example, if your package is named package-name
and the bad version of your package is v1.2.3 then the command you would execute is:
$ pulsar -p unpublish <package-name@1.2.3>
+
This will remove just this particular version from the https://pulsar-edit.dev package registry.
If you need to rename your package for any reason, you can do so with one simple command – pulsar -p publish --rename
changes the name
field in your package's package.json
, pushes a new commit and tag, and publishes your renamed package. Requests made to the previous name will be forwarded to the new name.
$ pulsar -p publish --rename <new-package-name>
+
Tips
Tip: Once a package name has been used, it cannot be re-used by another package even if the original package is unpublished.
Pulsar supports language-specific settings. You can soft wrap only Markdown files, or set the tab length to 4 in Python files.
Language-specific settings are a subset of something more general we call "scoped settings". Scoped settings allow targeting down to a specific syntax token type. For example, you could conceivably set a setting to target only Ruby comments, only code inside Markdown files, or even only JavaScript function names.
Each token in the editor has a collection of scope names. For example, the aforementioned JavaScript function name might have the scope names function
and name
. An open paren might have the scope names punctuation
, parameters
, begin
.
Scope names work just like CSS classes. In fact, in the editor, scope names are attached to a token's DOM node as CSS classes.
Take this piece of JavaScript:
function functionName() {
+ console.log("Log it out");
+}
+
In the dev tools, the first line's markup looks like this.
All the class names on the spans are scope names. Any scope name can be used to target a setting's value.
Scope selectors allow you to target specific tokens just like a CSS selector targets specific nodes in the DOM. Some examples:
'.source.js' # selects all javascript tokens
+'.source.js .function.name' # selects all javascript function names
+'.function.name' # selects all function names in any language
+
Config::set
accepts a scopeSelector
. If you'd like to set a setting for JavaScript function names, you can give it the JavaScript function name scopeSelector
:
atom.config.set("my-package.my-setting", "special value", {
+ scopeSelector: ".source.js .function.name",
+});
+
A scope descriptor is an Object that wraps an Array
of String
s. The Array describes a path from the root of the syntax tree to a token including all scope names for the entire path.
In our JavaScript example above, a scope descriptor for the function name token would be:
["source.js", "meta.function.js", "entity.name.function.js"];
+
Config::get
accepts a scopeDescriptor
. You can get the value for your setting scoped to JavaScript function names via:
const scopeDescriptor = [
+ "source.js",
+ "meta.function.js",
+ "entity.name.function.js",
+];
+const value = atom.config.get("my-package.my-setting", {
+ scope: scopeDescriptor,
+});
+
But, you do not need to generate scope descriptors by hand. There are a couple methods available to get the scope descriptor from the editor:
Editor::getRootScopeDescriptor
to get the language's descriptor. For example: [".source.js"]
Editor::scopeDescriptorForBufferPosition
to get the descriptor at a specific position in the buffer.Cursor::getScopeDescriptor
to get a cursor's descriptor based on position. eg. if the cursor were in the name of the method in our example it would return ["source.js", "meta.function.js", "entity.name.function.js"]
Let's revisit our example using these methods:
const editor = atom.workspace.getActiveTextEditor();
+const cursor = editor.getLastCursor();
+const valueAtCursor = atom.config.get("my-package.my-setting", {
+ scope: cursor.getScopeDescriptor(),
+});
+const valueForLanguage = atom.config.get("my-package.my-setting", {
+ scope: editor.getRootScopeDescriptor(),
+});
+
When a window is refreshed or restored from a previous session, the view and its associated objects are deserialized from a JSON representation that was stored during the window's previous shutdown. For your own views and objects to be compatible with refreshing, you'll need to make them play nicely with the serializing and deserializing.
Your package's main module can optionally include a serialize
method, which will be called before your package is deactivated. You should return a JSON-serializable object, which will be handed back to you as an object argument to activate
next time it is called. In the following example, the package keeps an instance of MyObject
in the same state across refreshes.
module.exports = {
+ activate(state) {
+ this.myObject = state
+ ? atom.deserializers.deserialize(state)
+ : new MyObject("Hello");
+ },
+
+ serialize() {
+ return this.myObject.serialize();
+ },
+};
+
class MyObject {
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
serialize()
Objects that you want to serialize should implement .serialize()
. This method should return a serializable object, and it must contain a key named deserializer
whose value is the name of a registered deserializer that can convert the rest of the data to an object. It's usually just the name of the class itself.
The other side of the coin is deserializers, whose job is to convert a state object returned from a previous call to serialize
back into a genuine object.
deserializers
in package.json
The preferred way to register deserializers is via your package's package.json
file:
{
+ "name": "wordcount",
+ ...
+ "deserializers": {
+ "MyObject": "deserializeMyObject"
+ }
+}
+
Here, the key ("MyObject"
) is the name of the deserializer—the same string used by the deserializer
field in the object returned by your serialize()
method. The value ("deserializeMyObject"
) is the name of a function in your main module that'll be passed the serialized data and will return a genuine object. For example, your main module might look like this:
module.exports = {
+ deserializeMyObject({ data }) {
+ return new MyObject(data);
+ },
+};
+
Now you can call the global deserialize
method with state returned from serialize
, and your class's deserialize
method will be selected automatically.
An alternative is to use the atom.deserializers.add
method with your class in order to make it available to the deserialization system. Usually this is used in conjunction with a class-level deserialize
method:
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+ }
+
+ static deserialize({ data }) {
+ return new MyObject(data);
+ }
+
+ constructor(data) {
+ this.data = data;
+ }
+
+ serialize() {
+ return {
+ deserializer: "MyObject",
+ data: this.data,
+ };
+ }
+}
+
+MyObject.initClass();
+
While this used to be the standard method of registering a deserializer, the package.json
method is now preferred since it allows Pulsar to defer loading and executing your code until it's actually needed.
class MyObject {
+ static initClass() {
+ atom.deserializers.add(this);
+
+ this.version = 2;
+ }
+
+ static deserialize(state) {
+ // ...
+ }
+
+ serialize() {
+ return {
+ version: this.constructor.version,
+ // ...
+ };
+ }
+}
+
+MyObject.initClass();
+
Your serializable class can optionally have a class-level @version
property and include a version
key in its serialized state. When deserializing, Pulsar will only attempt to call deserialize if the two versions match, and otherwise return undefined.
You should now have a better understanding of some of the core Pulsar APIs and systems.
Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
Now it's time to come to the "Hackable" part of the Hyper-Hackable Editor. As we've seen throughout the second section, a huge part of Pulsar is made up of bundled packages. If you wish to add some functionality to Pulsar, you have access to the same APIs and tools that the core features of Pulsar has. From the tree-view to the command-palette to find-and-replace functionality, even the most core features of Pulsar are implemented as packages.
In this section, we're going to learn how to extend the functionality of Pulsar through writing packages. This will be everything from new user interfaces to new language grammars to new themes. We'll learn this by writing a series of increasingly complex packages together, introducing you to new APIs and tools and techniques as we need them.
First though we will look at how to build the editor itself from source.
If you're looking for an example or specific section using a specific API or feature, you can check below for an index to this section and skip right to it.
If you want to investigate a bug, implement a new feature in Pulsar's core or just want to tinker then you will need to build and run Pulsar from source.
The Pulsar application code can be found in the pulsar-edit/pulsar repository.
To build Pulsar you will need to meet some basic requirements:
corepack enable
)For OS or distribution specific instructions see below:
To build the application so you can start hacking on the core you will need to download the source code to your local machine and cd
to the pulsar directory:
git clone https://github.com/pulsar-edit/pulsar.git && cd pulsar
+
Install Node.js (using nvm
- see above) and enable corepack (for yarn
). This will install the version of Node.js specified in pulsar/.nvmrc:
nvm install
+corepack enable
+
If Node.js is already installed, run the following to make sure the correct version of Node.js is being used (see requirements):
nvm use
+node -v
+
Run the following to initialize and update the submodules:
git submodule init && git submodule update
+
Now install and build Pulsar & ppm:
yarn install
+yarn build
+yarn build:apm
+
Start Pulsar!
yarn start
+
These instructions will also build ppm
(Pulsar Package Manager) but it will require some additional configuration for use.
The following will allow you to build Pulsar as a stand alone binary or installer. After running you will find your your built application in pulsar/binaries
.
The build script will automatically build for your system's CPU architecture, for example building on an x86_64
CPU will produce binaries for x86_64
, building on arm64
will only produce binaries for arm64
.
It is not possible to "cross-build" for different OSs. For Linux binaries you must build from a Linux machine, macOS binaries must be built from macOS etc. Your OS is detected automatically and the script will build the correct binaries for it.
ppm
is used for installing and managing Pulsar's packages in much the same way that apm
did on Atom. However at this point in the project there are a few hoops you have to jump through to get it to work correctly.
After following the build instructions you will find the ppm
binary at pulsar/ppm/bin/apm
but by default Pulsar will be looking in the wrong place. There will also be issues relating to the Electron version which will prevent install from the package backend. To solve this a couple of environmental variables need to be exported.
You can now use the binary to link or install packages.
For example to install the ide-java
package from source:
# clone the repository and cd into it
+git clone https://github.com/pulsar-edit/ide-java
+cd ide-java
+
+# from the directory where you are running pulsar source code
+<pulsar source>/ppm/bin/apm link
+
You will first want to build and run Pulsar from source.
Once you have a local copy of Pulsar cloned and built, you can then run Pulsar in Development Mode. But first, if you cloned Pulsar to somewhere other than LNX/MAC: ~/github/pulsar
- WIN: %USERPROFILE%\github\pulsar
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Pulsar. To run Pulsar in Dev Mode, use the --dev
parameter from the terminal:
$ pulsar --dev <path-to-open>
+
There are a couple benefits of running Pulsar in Dev Mode:
ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Pulsar is run using the source code from your local pulsar-edit/pulsar
repository. This means you don't have to rebuild after every change, just restart Pulsar 👍~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\dev\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.window:reload
) to see changes to those.In order to run Pulsar Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <path-to-your-local-pulsar-repo>
+$ pulsar --test spec
+
To begin, there are a few things we'll assume you know, at least to some degree. Since all of Pulsar is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.
While much of Pulsar has been converted to JavaScript, a lot of older code is still implemented in CoffeeScript but the process of "decaffeination" is ongoing, to continue this conversion feel free to read more. Additionally, Pulsar's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:
MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
We'll go over examples like this in a bit, but this is what the language looks like. Just about everything you can do with CoffeeScript in Pulsar is also doable in JavaScript. You can brush up on CoffeeScript at coffeescript.org.
Less is an even simpler transition from CSS. It adds a number of useful things like variables and functions to CSS. You can learn about Less at lesscss.org. Our usage of Less won't get too complex in this book however, so as long as you know basic CSS you should be fine.
Info
The default init file for Pulsar has been changed from the previous CoffeeScript init.coffee
file used by Atom to JavaScript. The CoffeeScript file will still work but should you wish to reference the specific version of this document for it then you should look at the Atom Archive.
When Pulsar finishes loading, it will evaluate init.js
in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory, giving you a chance to run JavaScript code to make customizations. Code in this file has full access to Pulsar's API. If customizations become extensive, consider creating a package, which we will cover in Package: Word Count.
You can open the init.js
file in an editor from the LNX: Atom > Init Script - MAC: File > Init Script - WIN: Edit > Init Script menu.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.js
file to have Pulsar greet you with an audio beep every time it loads:
atom.beep();
+
Because init.js
provides access to Pulsar's API, you can use it to implement useful commands without creating a new package or extending an existing one. Here's a command which uses the Selection API and Clipboard API to construct a Markdown link from the selected text and the clipboard contents as the URL:
atom.commands.add("atom-text-editor", "markdown:paste-as-link", () => {
+ let clipboardText, editor, selection;
+ if (!(editor = atom.workspace.getActiveTextEditor())) {
+ return;
+ }
+ selection = editor.getLastSelection();
+ clipboardText = atom.clipboard.read();
+ return selection.insertText(
+ "[" + selection.getText() + "](" + clipboardText + ")"
+ );
+});
+
Now, reload Pulsar and use the Command Palette to execute the new command, Markdown: Paste As Link
, by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a keybinding for the command.
Let's get started by writing a very simple package and looking at some of the tools needed to develop one effectively. We'll start by writing a package that tells you how many words are in the current buffer and display it in a small modal window.
The simplest way to start a package is to use the built-in package generator that ships with Pulsar. As you might expect by now, this generator is itself a separate package implemented in package-generator.
You can run the generator by invoking the command palette and searching for "Generate Package". A dialog will appear asking you to name your new project. Name it your-name-word-count
. Pulsar will then create that directory and fill it out with a skeleton project and link it into your LNX/MAC: ~/.pulsar/packages
- WIN: %USERPROFILE%\.pulsar\packages
directory so it's loaded when you launch your editor next time.
Note
You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on pulsar-edit.dev (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the your-name-word-count
package name, you should be safe 😀
You can see that Pulsar has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+├─ grammars/
+├─ keymaps/
+├─ lib/
+├─ menus/
+├─ spec/
+├─ snippets/
+├─ styles/
+├─ index.js
+└─ package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
Similar to Node modules, Pulsar packages contain a package.json
file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.
In addition to some of the regular Node package.json
keys available, Pulsar package.json
files have their own additions.
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Pulsar will default to looking for an index.js
or index.coffee
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Pulsar has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Pulsar's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Pulsar. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
activate(state)
: This optional method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the serialize()
method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).initialize(state)
: This optional method is similar to activate()
but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after your package's deserializers have been called), initialize()
is guaranteed to be called before everything. Use activate()
if you want to be sure that the workspace is ready; use initialize()
if you need to do some setup prior to your deserializers or view providers being invoked.serialize()
: This optional method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's activate
method so you can restore your view to where the user left off.deactivate()
: This optional method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.Style sheets for your package should be placed in the styles
directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or Less, but Less is recommended.
Ideally, you won't need much in the way of styling. Pulsar provides a standard set of components which define both the colors and UI elements for any package that fits into Pulsar seamlessly. You can view all of Pulsar's UI components by opening the styleguide: open the command palette LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or type LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G
If you do need special styling, try to keep only structural styles in the package style sheets. If you must specify colors and sizing, these should be taken from the active theme's ui-variables.less.
An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Pulsar UI, this means the key combination will work anywhere in the application.
We'll cover more advanced keybinding stuff a bit later in Keymaps in Depth.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your package.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Pulsar text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole package.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Pulsar but optional. The toggle
method is one Pulsar is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Pulsar starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Pulsar workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple package. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the package so other packages could subscribe to these events.
// Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling atom.workspace.getActiveTextEditor()
.
Next we get the number of words by calling getText()
on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = `There are ${count} words.`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Pulsar windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Pulsar being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Pulsar.
Under the hood, Jasmine v1.3 executes your tests, so you can assume that any DSL available there is also available to your package.
Once you've got your test suite written, you can run it by pressing LNX/WIN: Alt+Ctrl+P - MAC: Alt+Cmd+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the pulsar --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
We've now generated, customized and tested our first package for Pulsar. Congratulations! Now let's go ahead and publish it so it's available to the world.
Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with ascii art. When you run our new command with the word "cool" selected, it will be replaced with:
o888 + ooooooo ooooooo ooooooo 888 + 888 888 888 888 888 888 888 + 888 888 888 888 888 888 + 88ooo888 88ooo88 88ooo88 o888o + +
This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.
The final package can be viewed at https://github.com/pulsar-edit/ascii-art.
To begin, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P to bring up the Command Palette. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter ascii-art
as the name of the package.
Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
As in Counting Words, we're using atom.workspace.getActiveTextEditor()
to get the object that represents the active text editor. If this convert()
method is called when not focused on a text editor, nothing will happen.
Next we insert a string into the current text editor with the insertText()
method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L
Now open the Command Palette and search for the Ascii Art: Convert
command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Pulsar launch faster by allowing Pulsar to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the Ascii Art: Convert
command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
The Pulsar keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
Now we need to convert the selected text to ASCII art. To do this we will use the figlet Node module from npm. Open package.json
and add the latest version of figlet to the dependencies:
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command Update Package Dependencies: Update
from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run Update Package Dependencies: Update
whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(`\n${art}\n`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
There are a couple of new things in this example we should look at quickly. The first is the editor.getSelectedText()
which, as you might guess, returns the text that is currently selected.
We then call the Figlet code to convert that into something else and replace the current selection with it with the editor.insertText()
call.
In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.
We saw in our Word Count package how we could show information in a modal panel. However, panels aren't the only way to extend Pulsar's UI—you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Pulsar will restore them the next time you open the project.
For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at https://github.com/pulsar-edit/active-editor-info.
To begin, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P to bring up the Command Palette. Type "generate package" and select the Package Generator: Generate Package
command, just as we did in the section on package generation. Enter active-editor-info
as the name of the package.
Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Pulsar. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Pulsar will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Pulsar uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Pulsar for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Pulsar to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the Active Editor Info: Toggle
command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = `
+ <h2>${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> ${item.softWrapped}</li>
+ <li><b>Tab Length:</b> ${item.getTabLength()}</li>
+ <li><b>Encoding:</b> ${item.getEncoding()}</li>
+ <li><b>Line Count:</b> ${item.getLineCount()}</li>
+ </ul>
+ `;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Pulsar how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Pulsar and toggle the view with the Active Editor Info: Toggle
command. Then reload Pulsar again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
Pulsar's interface is rendered using HTML, and it's styled via Less which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.
Pulsar supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
package.json
(as covered in Pulsar package.json
). This file is used to help distribute your theme to Pulsar users.package.json
must contain a theme
key with a value of ui
or syntax
for Pulsar to recognize and load it as a theme.Let's create your first theme.
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing Generate Syntax Theme
to generate a new theme package. Select Generate Syntax Theme
, and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Syntax themes should end with -syntax and UI themes should end with -ui.
Pulsar will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Pulsar by pressing LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L to see the changes you made reflected in your Pulsar window. Pretty neat!
Tip
You can avoid reloading to see changes you make by opening an Pulsar window in Dev Mode. To open a Dev Mode Pulsar window run pulsar --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Pulsar's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
pulsar --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
pulsar -p link --dev
to symlink your repository to LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar
Tip
Because we used pulsar -p link --dev
in the above instructions, if you break anything you can always close Pulsar and launch Pulsar normally to force Pulsar to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
These default values will be used as a fallback in case a theme doesn't define its own variables.
In any of your package's .less
files, you can access the theme variables by importing the ui-variables
or syntax-variables
file from Pulsar.
Your package should generally only specify structural styling, and these should come from the style guide. Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!
Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
Reloading by pressing LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L after you make changes to your theme is less than ideal. Pulsar supports live updating of styles on Pulsar windows in Dev Mode.
To launch a Dev Mode window:
pulsar --dev
If you'd like to reload all the styles at any time, you can use the shortcut LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L,
Pulsar is based on the Chromium browser and supports its Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I
The dev tools allow you to inspect elements and take a look at their CSS properties.
Check out Google's extensive tutorial for a short introduction.
If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The Styleguide is a page that renders every component Pulsar supports.
To open the Styleguide, open the command palette with LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or use the shortcut LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes unusable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Pulsar in "normal" mode to fix the issue, it's advised to open two Pulsar windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Pulsar users, it's time to publish it. 🎉
Follow the steps on the Publishing page. The example used is for the Word Count package, but publishing a theme works exactly the same.
Pulsar's syntax highlighting and code folding system is powered by Tree-sitter. Tree-sitter parsers create and maintain full syntax trees representing your code.
This syntax tree gives Pulsar a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Pulsar are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Pulsar: a parser and a grammar file.
Tree-sitter generates parsers based on context-free grammars that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Pulsar.
They can also be developed and tested at the command line, separately from Pulsar. Tree-sitter has its own documentation page on how to create these parsers. The Tree-sitter GitHub organization also contains a lot of example parsers that you can learn from, each in its own repository.
Once you have created a parser, you need to publish it to the NPM registry to use it in Pulsar. To do this, make sure you have a name
and version
in your parser's package.json
:
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Pulsar package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+├── LICENSE
+├── README.md
+├── grammars
+│ └── mylanguage.cson
+└── package.json
+
The mylanguage.cson
file specifies how Pulsar should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
scopeName
- A unique, stable identifier for the language. Pulsar users will use this in configuration files if they want to specify custom configuration based on the language.name
- A human readable name for the language.parser
- The name of the parser node module that will be used for parsing. This string will be passed directly to require()
in order to load the parser.type
- This should have the value tree-sitter
to indicate to Pulsar that this is a Tree-sitter grammar and not a TextMate grammar.Next, the file should contain some fields that indicate to Pulsar when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Pulsar uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
Note that in this selector, we're using the immediate child combinator (>
). Arbitrary descendant selectors without this combinator (for example 'call_expression identifier'
, which would match any identifier
occurring anywhere within a call_expression
) are currently not supported.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
You can use the :nth-child
pseudo-class to select nodes based on their order within their parent. For example, this example selects identifier
nodes which are the fourth (zero-indexed) child of a singleton_method
node.
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
Finally, you can use double-quoted strings in the selectors to select anonymous tokens in the syntax tree, like (
and :
. See the Tree-sitter documentation for more information about named vs anonymous tokens.
scopes:
+ '''
+ "*",
+ "/",
+ "+",
+ "-"
+ ''': 'keyword.operator'
+
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:
+
+ # Apply the classes `syntax--builtin` and `syntax--variable` to all
+ # `identifier` nodes whose text is `require`.
+ 'identifier': {exact: 'require', scopes: 'builtin.variable'},
+
+ # Apply the classes `syntax--type` and `syntax--integer` to all
+ # `primitive_type` nodes whose text starts with `int` or `uint`.
+ 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},
+
+ # Apply the classes `syntax--builtin`, `syntax--class`, and
+ # `syntax--name` to `constant` nodes with the text `Array`,
+ # `Hash` and `String`. For all other `constant` nodes, just
+ # apply the classes `syntax--class` and `syntax--name`.
+ 'constant': [
+ {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},
+ 'class.name'
+ ]
+
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.If multiple selectors in the scopes
object match a node, the node's classes will be decided based on the most specific selector. Note that the exact
and match
rules do not affect specificity, so you may need to supply the same exact
or match
rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector `call_expression > identifier` is more
+ # specific than the selector `identifier`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
For example, in JavaScript, tagged template literals sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:
// HTML in a template literal
+const htmlContent = html`<div>Hello ${name}</div>`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All `comment` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # `if_statement` nodes are foldable if they contain an anonymous
+ # "then" token and either an `elif_clause` or `else_clause` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # `elif_clause` or `else_clause`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Pulsar's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or uncomment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Pulsar:
Pulsar's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Pulsar's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Pulsar uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
Grammar files are written in the CSON or JSON format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Grammar packages should start with language-.
The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the keymaps
, lib
, menus
, and styles
folders. Furthermore, in package.json
, remove the activationCommands
section. Now create a new folder called grammars
, and inside that a file called flight-manual.cson
. This is the main file that we will be working with - start by populating it with a boilerplate template. Now let's go over what each key means.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \bFlight Manual\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\bFlight Manual\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
match
is where your regex is contained, and name
is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in Section 12.4 of the TextMate Manual.
Tip
All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Astute readers may have noticed that the \b
was changed to \\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\b(Flight) (Manual)\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the {{#note}}
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. {{#note}}
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\b(Flight) (Manual)\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
first-mate
. Not necessary to write a grammar, but a good technical reference for what Pulsar is doing behind the scenes.It's possible that you have themes or grammars from TextMate that you like and use and would like to convert to Pulsar. If so, you're in luck because there are tools to help with the conversion.
Converting a TextMate bundle will allow you to use its editor preferences, snippets, and colorization inside Pulsar.
Let's convert the TextMate bundle for the R programming language. You can find other existing TextMate bundles on GitHub.
You can convert the R bundle with the following command:
$ pulsar -p init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the pulsar -p link
command, your new package is ready to use. Launch Pulsar and open a .r
file in the editor to see it in action!
This section will go over how to convert a TextMate theme to an Pulsar theme.
TextMate themes use plist files while Pulsar themes use CSS or Less to style the UI and syntax in the editor.
The utility that converts the theme first parses the theme's plist file and then creates comparable CSS rules and properties that will style Pulsar similarly.
Download the theme you wish to convert.
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ pulsar -p init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Pulsar and opening the Settings View with the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
Pulsar bundles a command line utility called ppm
which we first used back in Command Line to search for and install packages via the command line. This is invoked by using the pulsar
command with the -p
or --package
option. The pulsar -p
command can also be used to publish Pulsar packages to the public registry and update them.
See more in Using PPM.
There are a few things you should double check before publishing:
package.json
file has name
, description
, and repository
fields.package.json
name
is URL Safe, as in it's not an emoji or special character.package.json
file has a version
field with a value of "0.0.0"
.package.json
version
field is Semver V2 compliant.package.json
file has an engines
field that contains an entry for atom
such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Before you publish a package it is a good idea to check ahead of time if a package with the same name has already been published to the Pulsar Package Repository. You can do that by visiting https://web.pulsar-edit.dev/packages/your-package-name
to see if the package already exists. If it does, update your package's name to something that is available before proceeding.
Now let's review what the pulsar -p publish
command does:
version
field in the package.json
file and commits it.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ pulsar -p publish minor
+
If this is the first package you are publishing, the pulsar -p publish
command may prompt you for your GitHub username and password. If you have two-factor authentication enabled, use a personal access token in lieu of a password. This is required to publish and you only need to enter this information the first time you publish. The credentials are stored securely in your keychain once you login.
Your package is now published and available on Pulsar Package Repository. Head on over to https://web.pulsar-edit.dev/packages/your-package-name
to see your package's page.
With pulsar -p publish
, you can bump the version and publish by using
$ pulsar -p publish <version-type>
+
where version-type
can be major
, minor
and patch
.
i.e. to bump a package from v1.0.0 to v1.1.0:
$ pulsar -p publish minor
+
Check out semantic versioning to learn more about best practices for versioning your package releases.
You can also run pulsar -p help publish
to see all the available options and pulsar -p help
to see all the other available commands.
Pulsar comes bundled with the Octicons 4.4.0 icon set. Use them to add icons to your packages.
NOTE: Some older icons from version
2.1.2
are still kept for backwards compatibility.
In the Styleguide under the "Icons" section you'll find all the Octicons that are available.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
Although icons can make your UI visually appealing, when used without a text label, it can be hard to guess its meaning. In cases where space for a text label is insufficient, consider adding a tooltip that appears on hover. Or a more subtle title="label"
attribute would help as well.
Pulsar provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when submitting issues:
You might be running into an issue which was already fixed in a more recent version of Pulsar than the one you're using.
If you're using a released version, check which version of Pulsar you're using:
$ pulsar --version
+> Pulsar : 1.63.0-dev
+> Electron: 12.2.3
+> Chrome : 89.0.4389.128
+> Node : 14.16.0
+
You can find the latest releases on the Pulsar Website, follow the links for either the latest release or Cirrus CI version. Make sure to mention which version when logging an issue.
If you're building Pulsar from source, pull down the latest version of master and re-build. Make sure that if logging an issue you include the latest commit hash you built from.
A large part of Pulsar's functionality comes from packages you can install. Pulsar will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Pulsar from the terminal in safe mode:
$ pulsar --safe
+
This starts Pulsar, but does not load packages from LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\packages
or %USERPROFILE%\.pulsar\dev\packages
. and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Pulsar normally still produces the error, then try figuring out which package is causing trouble. Start Pulsar normally again and open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Pulsar or reload Pulsar with LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L. after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Pulsar saves a number of things about your environment when you exit in order to restore Pulsar to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Pulsar from working properly. In these cases, you may want to clear the state that Pulsar has saved.
DANGER
Clearing the saved state permanently destroys any state that Pulsar has saved across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ pulsar --clear-window-state
+
In some cases, you may want to reset Pulsar to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
Once that is complete, you can launch Pulsar as normal. Everything will be just as if you first installed Pulsar.
Tip
The command given above doesn't delete the old configuration, just puts it somewhere that Pulsar can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the LNX/MAC: ~/.pulsar-backup
- WIN: %USERPROFILE%\.pulsar-backup
directory.
If you develop or contribute to Pulsar packages, there may be left-over packages linked to your LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\packages
or %USERPROFILE%\.pulsar\dev\packages
. directories. You can use the pulsar -p links
command to list all linked packages:
$ pulsar -p links
+> /Users/pulsy/.pulsar/dev/packages (0)
+> └── (no links)
+> /Users/pulsy/.pulsar/packages (1)
+> └── color-picker -> /Users/pulsy/github/color-picker
+
You can remove links using the pulsar -p unlink
command:
$ pulsar -p unlink color-picker
+> Unlinking /Users/pulsy/.pulsar/packages/color-picker ✓
+
See pulsar -p links --help
and pulsar -p unlink --help
for more information on these commands.
Tip
You can also use pulsar -p unlink --all
to easily unlink all packages and themes.
If you have packages installed that use native Node modules, when you upgrade to a new version of Pulsar, they might need to be rebuilt. Pulsar detects this and through the incompatible-packages package displays an indicator in the status bar when this happens.
If you see this indicator, click it and follow the instructions.
In some cases, unexpected behavior might be caused by settings in Pulsar or in one of the packages.
Open Pulsar's Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+,, the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu option, or the Settings View: Open
command from the Command Palette.
Check Pulsar's settings in the Settings View, there's a description of most configuration options in the Basic Customization section. For example, if you want Pulsar to hide the invisible symbols representing whitespace characters, disable the "Show Invisibles" option.
Some of these options are also available on a per-language basis which means that they may be different for specific languages, for example JavaScript or Python. To check the per-language settings, open the settings for the language package under the Packages tab in the Settings View, for example the language-javascript or language-python package.
Since Pulsar ships with a set of packages and you can also install additional packages yourself, check the list of packages and their settings. For instance, if you'd like to get rid of the vertical line in the middle of the editor, disable the Wrap Guide package. And if you don't like it when Pulsar strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the whitespace package's settings.
You might have defined some custom styles, keymaps or snippets in one of your configuration files. In some situations, these personal hacks might be causing the unexpected behavior you're observing so try clearing those files and restarting Pulsar.
If a command is not executing when you press a key combination or the wrong command is executing, there might be an issue with the keybinding for that combination. Pulsar ships with the Keybinding Resolver, a neat package which helps you understand what key Pulsar saw you press and the command that was triggered because of it.
Show the keybinding resolver with LNX/WIN: Ctrl+. - MAC: Cmd+., or with Keybinding Resolver: Show
from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
If multiple keybindings are matched, Pulsar determines which keybinding will be executed based on the specificity of the selectors and the order in which they were loaded. If the command you wanted to trigger is listed in the Keybinding Resolver, but wasn't the one that was executed, this is normally explained by one of two causes:
The key combination was not used in the context defined by the keybinding's selector
For example, you can't trigger the keybinding for the tree-view:add-file
command if the Tree View is not focused.
There is another keybinding that took precedence
This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.
Pulsar loads core Pulsar keybindings and package keybindings first, and user-defined keybindings last. Since user-defined keybindings are loaded last, you can use your keymap.cson
file to tweak the keybindings and sort out problems like these. See the Keymaps in Depth section for more information.
If you notice that a package's keybindings are taking precedence over core Pulsar keybindings, it might be a good idea to report the issue on that package's GitHub repository. You can contact Pulsar maintainers on Pulsar's github discussions.
You can determine which fonts are being used to render a specific piece of text by using the Developer Tools. To open the Developer Tools press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I. Once the Developer Tools are open, click the "Elements" tab. Use the standard tools for finding the element containing the text you want to check. Once you have selected the element, you can click the "Computed" tab in the styles pane and scroll to the bottom. The list of fonts being used will be shown there:
When an unexpected error occurs in Pulsar, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
If Pulsar is taking a long time to start, you can use the Timecop package to get insight into where Pulsar spends time while loading.
Timecop displays the following information:
If a specific package has high load or activation times, you might consider reporting an issue to the maintainers. You can also disable the package to potentially improve future startup times.
If you're experiencing performance problems in a particular situation, your Issue reports will be more valuable if you include a saved profile from Chrome's CPU profiler that gives some insight into what is slow.
To run a profile, open the Developer Tools with LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
To learn more, check out the Chrome documentation on CPU profiling.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Pulsar:
$ pulsar --profile-startup .
+
This will automatically capture a CPU profile as Pulsar is loading and open the Developer Tools once Pulsar loads. From there:
You can then include the startup profile in any issue you report.
If you are having issues installing a package using pulsar -p install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run pulsar -p install --check
to see if the Pulsar package manager can build native code on your machine.
Check out the pre-requisites in the build instructions for your platform for more details.
If you encounter flickering or other rendering issues, you can stop Pulsar from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ pulsar --disable-gpu
+
Chromium (and thus Pulsar) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Pulsar to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage on portable devices as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ pulsar --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Pulsar flags as they will not be executed after the Chromium flags e.g.:
$ pulsar --safe --enable-gpu-rasterization --force-gpu-rasterization
+
We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Pulsar?
Pulsar uses Jasmine as its spec framework. Any new functionality should have specs to guard against regressions.
Pulsar specs and package specs are added to their respective spec
directory. The example below creates a spec for Pulsar core.
Spec files must end with -spec
so add sample-spec.js
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
The best way to learn about expectations is to read the Jasmine documentation about them. Below is a simple example.
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Pulsar includes the following:
toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domThese are defined in spec/spec-helper.js.
Writing Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Pulsar. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
For a more detailed documentation on asynchronous tests please visit the Jasmine documentation.
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Pulsar core specs when working on Pulsar itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the Travis CI For Your Packages and AppVeyor CI For Your Packages posts for more details.
To run tests on the command line, run Pulsar with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
pulsar --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Pulsar will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Pulsar will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Pulsar's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.pulsar
).enablePersistence
A boolean indicating whether the Pulsar environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via pulsar --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finished running. This exit code will be returned when running your tests via the command line.
Packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Pulsar package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Pulsar which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Pulsar from deferring activation of your package — see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Now that we've told Pulsar that we want our package to handle URIs beginning with atom://my-package/
via our handleURI
method, we need to actually write this method. Pulsar passes two arguments to your URI handler method; the first one is the fully-parsed URI plus query string, parsed with Node's url.parse(uri, true)
. The second argument is the raw, string URI; this is normally not needed since the first argument gives you structured information about the URI.
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Pulsar handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings — you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Pulsar will not activate your package until it has a URI for it to handle — it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately — disabling deferred activation means Pulsar takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Pulsar on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger Pulsar with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Pulsar provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Pulsar runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)/my\ test
"c:\my test"
\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\
and d:\
Creation and deletion operations may take a few milliseconds to complete. If you need to remove many files and folders consider RimRAF which has built-in retry logic for this.
LF
CRLF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
If you discover a bug or issue with an official Pulsar package then feel free to open up the issue in that specific repository instead. When in doubt just open the issue on the pulsar-edit/pulsar repository but be aware that it may get transferred to the proper package's repository.
The first step is creating your own clone. For some packages, you may also need to install the requirements necessary for building Pulsar in order to run pulsar -p install
.
For example, if you want to make changes to the tree-view
package, fork the repo on your GitHub account, then clone it:
$ git clone https://github.com/pulsar-edit/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ pulsar -p install
+> Installing modules ✓
+
Now you can link it to development mode so when you run an Pulsar window with pulsar -p --dev
, you will use your fork instead of the built in package:
$ pulsar -p link -d
+
Editing a package in Pulsar is a bit of a circular experience: you're using Pulsar to modify itself. What happens if you temporarily break something? You don't want the version of Pulsar you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the Application: Open Dev
command. You can also run dev mode from the command line with pulsar --dev
.
To load your package in development mode, create a symlink to it in LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\dev\packages
. This occurs automatically when you clone the package with pulsar -p develop
. You can also run pulsar -p link --dev
and pulsar -p unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running pulsar -p update
after pulling any upstream changes.
Several of Pulsar's core packages are maintained in the packages
directory of the pulsar-edit/pulsar repository. If you would like to use one of these packages as a starting point for your own package, please follow the steps below.
Tip
In most cases, we recommend generating a brand new package or a brand new theme as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Pulsar package.
For the sake of this guide, let's assume that you want to start with the current code in the one-light-ui package, make some customizations to it, and publish your new package under the name "one-light-ui-plus".
Download the current contents of the pulsar-edit/pulsar repository as a zip file
Unzip the file to a temporary location (for example LNX/MAC: /tmp/pulsar
- WIN: C:\TEMP\pulsar
)
Copy the contents of the desired package into a working directory for your fork
$ git init
+$ git commit -am "Import core Pulsar package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
Create a public repository on github.com for your new package
Follow the instructions in the github.com UI to push your code to your new online repository
Follow the steps in the Publishing guide to publish your new package
The code in the original package will continue to evolve over time, either to fix bugs or to add new enhancements. You may want to incorporate some or all of those updates into your package. To do so, you can follow these steps for merging upstream changes into your package.
Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team consolidated many core Atom packages into the atom/atom repository. For example, the one-light-ui package was originally maintained in the atom/one-light-ui repository, but was moved to the packages/one-light-ui
directory in the main repository.
The Pulsar team has continued this trend and has move even more packages into the core, particularly default language packages. A list of these packages moved can be found in this document.
If you forked one of the core packages before it was moved into the atom/atom or pulsar-edit/pulsar repository, and you want to continue merging upstream changes into your fork, please follow the steps below.
For the sake of this guide, let's assume that you forked the pulsar-edit/one-light-ui repository, renamed your fork to one-light-ui-plus
, and made some customizations.
Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
Add the pulsar-edit/pulsar repository as a git remote:
$ git remote add upstream https://github.com/pulsar-edit/pulsar.git
+
Tip
Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the pulsar-edit/pulsar repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log --oneline upstream/master -- packages/one-light-ui
+f884f6de8 [themes] Rename A[a]tom -> P[p]ulsar
+0db3190f4 Additional rebranding where needed
+234adb874 Remove deprecated code strings
+...
+
Look through the log and identify the commits that you want to merge into your fork.
For each commit that you want to bring into your fork, use [git format-patch
][https://git-scm.com/docs/git-format-patch] in conjunction with [git am
][https://git-scm.com/docs/git-am]. For example, to merge commit f884f6de8
into your fork:
$ git format-patch -1 --stdout f884f6de8 | git am -p3
+
Repeat this step for each commit that you want to merge into your fork.
If you finished this chapter, you should be an Pulsar-hacking master. We've discussed how you should work with JavaScript and CoffeeScript, and how to put it to good use in creating packages. You should also be able to do this in your own created theme now.
Even when something goes wrong, you should be able to debug this easily. But also fewer things should go wrong, because you are capable of writing great specs for Pulsar.
In the next chapter, we’ll go into more of a deep dive on individual internal APIs and systems of Pulsar, even looking at some Pulsar source to see how things are really getting done.
If you have any issues then please feel free to ask for help from the Pulsar Team or the wider community via any of our Community areas
If you think you have found a bug then please have a look through our existing issues and if you can't find anything then please create a new bug report.
If you want to investigate a bug, implement a new feature in Pulsar's core or just want to tinker then you will need to build and run Pulsar from source.
The Pulsar application code can be found in the pulsar-edit/pulsar repository.
To build Pulsar you will need to meet some basic requirements:
corepack enable
)For OS or distribution specific instructions see below:
To build the application so you can start hacking on the core you will need to download the source code to your local machine and cd
to the pulsar directory:
git clone https://github.com/pulsar-edit/pulsar.git && cd pulsar
+
Install Node.js (using nvm
- see above) and enable corepack (for yarn
). This will install the version of Node.js specified in pulsar/.nvmrc:
nvm install
+corepack enable
+
If Node.js is already installed, run the following to make sure the correct version of Node.js is being used (see requirements):
nvm use
+node -v
+
Run the following to initialize and update the submodules:
git submodule init && git submodule update
+
Now install and build Pulsar & ppm:
yarn install
+yarn build
+yarn build:apm
+
Start Pulsar!
yarn start
+
These instructions will also build ppm
(Pulsar Package Manager) but it will require some additional configuration for use.
The following will allow you to build Pulsar as a stand alone binary or installer. After running you will find your your built application in pulsar/binaries
.
The build script will automatically build for your system's CPU architecture, for example building on an x86_64
CPU will produce binaries for x86_64
, building on arm64
will only produce binaries for arm64
.
It is not possible to "cross-build" for different OSs. For Linux binaries you must build from a Linux machine, macOS binaries must be built from macOS etc. Your OS is detected automatically and the script will build the correct binaries for it.
If you discover a bug or issue with an official Pulsar package then feel free to open up the issue in that specific repository instead. When in doubt just open the issue on the pulsar-edit/pulsar repository but be aware that it may get transferred to the proper package's repository.
The first step is creating your own clone. For some packages, you may also need to install the requirements necessary for building Pulsar in order to run pulsar -p install
.
For example, if you want to make changes to the tree-view
package, fork the repo on your GitHub account, then clone it:
$ git clone https://github.com/pulsar-edit/tree-view.git
+
Next install all the dependencies:
$ cd tree-view
+$ pulsar -p install
+> Installing modules ✓
+
Now you can link it to development mode so when you run an Pulsar window with pulsar -p --dev
, you will use your fork instead of the built in package:
$ pulsar -p link -d
+
Editing a package in Pulsar is a bit of a circular experience: you're using Pulsar to modify itself. What happens if you temporarily break something? You don't want the version of Pulsar you're using to edit to become useless in the process. For this reason, you'll only want to load packages in development mode while you are working on them. You'll perform your editing in stable mode, only switching to development mode to test your changes.
To open a development mode window, use the Application: Open Dev
command. You can also run dev mode from the command line with pulsar --dev
.
To load your package in development mode, create a symlink to it in LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\dev\packages
. This occurs automatically when you clone the package with pulsar -p develop
. You can also run pulsar -p link --dev
and pulsar -p unlink --dev
from the package directory to create and remove dev-mode symlinks.
You'll want to keep dependencies up to date by running pulsar -p update
after pulling any upstream changes.
It's possible that you have themes or grammars from TextMate that you like and use and would like to convert to Pulsar. If so, you're in luck because there are tools to help with the conversion.
Converting a TextMate bundle will allow you to use its editor preferences, snippets, and colorization inside Pulsar.
Let's convert the TextMate bundle for the R programming language. You can find other existing TextMate bundles on GitHub.
You can convert the R bundle with the following command:
$ pulsar -p init --package language-r --convert https://github.com/textmate/r.tmbundle
+
You can now change directory into language-r
to see the converted bundle. Once you link your package with the pulsar -p link
command, your new package is ready to use. Launch Pulsar and open a .r
file in the editor to see it in action!
This section will go over how to convert a TextMate theme to an Pulsar theme.
TextMate themes use plist files while Pulsar themes use CSS or Less to style the UI and syntax in the editor.
The utility that converts the theme first parses the theme's plist file and then creates comparable CSS rules and properties that will style Pulsar similarly.
Download the theme you wish to convert.
Now, let's say you've downloaded the theme to ~/Downloads/MyTheme.tmTheme
, you can convert the theme with the following command:
$ pulsar -p init --theme my-theme --convert ~/Downloads/MyTheme.tmTheme
+
You can then change directory to my-theme
to see the converted theme.
Once your theme is installed you can enable it by launching Pulsar and opening the Settings View with the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu item. Then select the "Themes" tab on the left side navigation. Finally, choose "My Theme" from the "Syntax Theme" dropdown menu to enable your new theme.
Your theme is now enabled, open an editor to see it in action!
Several of Pulsar's core packages are maintained in the packages
directory of the pulsar-edit/pulsar repository. If you would like to use one of these packages as a starting point for your own package, please follow the steps below.
Tip
In most cases, we recommend generating a brand new package or a brand new theme as the starting point for your creation. The guide below applies only to situations where you want to create a package that closely resembles a core Pulsar package.
For the sake of this guide, let's assume that you want to start with the current code in the one-light-ui package, make some customizations to it, and publish your new package under the name "one-light-ui-plus".
Download the current contents of the pulsar-edit/pulsar repository as a zip file
Unzip the file to a temporary location (for example LNX/MAC: /tmp/pulsar
- WIN: C:\TEMP\pulsar
)
Copy the contents of the desired package into a working directory for your fork
$ git init
+$ git commit -am "Import core Pulsar package"
+
Update the name
property in package.json
to give your package a unique name
Make the other customizations that you have in mind
Commit your changes
$ git commit -am "Apply initial customizations"
+
Create a public repository on github.com for your new package
Follow the instructions in the github.com UI to push your code to your new online repository
Follow the steps in the Publishing guide to publish your new package
The code in the original package will continue to evolve over time, either to fix bugs or to add new enhancements. You may want to incorporate some or all of those updates into your package. To do so, you can follow these steps for merging upstream changes into your package.
Pulsar's syntax highlighting and code folding system is powered by Tree-sitter. Tree-sitter parsers create and maintain full syntax trees representing your code.
This syntax tree gives Pulsar a comprehensive understanding of the structure of your code, which has several benefits:
Select Larger Syntax Node
and Select Smaller Syntax Node
allow you to select conceptually larger and smaller chunks of your code.Tree-sitter grammars are relatively new. Many languages in Pulsar are still supported by TextMate grammars, though we intend to phase these out over time.
If you're adding support for a new language, you're in the right place!
There are two components required to use Tree-sitter in Pulsar: a parser and a grammar file.
Tree-sitter generates parsers based on context-free grammars that are typically written in JavaScript. The generated parsers are C libraries that can be used in other applications as well as Pulsar.
They can also be developed and tested at the command line, separately from Pulsar. Tree-sitter has its own documentation page on how to create these parsers. The Tree-sitter GitHub organization also contains a lot of example parsers that you can learn from, each in its own repository.
Once you have created a parser, you need to publish it to the NPM registry to use it in Pulsar. To do this, make sure you have a name
and version
in your parser's package.json
:
{
+ "name": "tree-sitter-mylanguage",
+ "version": "0.0.1",
+ // ...
+}
+
then run the command npm publish
.
Once you have a Tree-sitter parser that is available on npm, you can use it in your Pulsar package. Packages with grammars are, by convention, always named starting with language. You'll need a folder with a package.json
, a grammars
subdirectory, and a single json
or cson
file in the grammars
directory, which can be named anything.
language-mylanguage
+├── LICENSE
+├── README.md
+├── grammars
+│ └── mylanguage.cson
+└── package.json
+
The mylanguage.cson
file specifies how Pulsar should use the parser you created.
It starts with some required fields:
name: 'My Language'
+scopeName: 'mylanguage'
+type: 'tree-sitter'
+parser: 'tree-sitter-mylanguage'
+
scopeName
- A unique, stable identifier for the language. Pulsar users will use this in configuration files if they want to specify custom configuration based on the language.name
- A human readable name for the language.parser
- The name of the parser node module that will be used for parsing. This string will be passed directly to require()
in order to load the parser.type
- This should have the value tree-sitter
to indicate to Pulsar that this is a Tree-sitter grammar and not a TextMate grammar.Next, the file should contain some fields that indicate to Pulsar when this language should be used. These fields are all optional.
fileTypes
- An array of filename suffixes. The grammar will be used for files whose names end with one of these suffixes. Note that the suffix may be an entire filename.firstLineRegex
- A regex pattern that will be tested against the first line of the file. The grammar will be used if this regex matches.contentRegex
- A regex pattern that will be tested against the contents of the file in order to break ties in cases where multiple grammars matched the file using the above two criteria. If the contentRegex
matches, this grammar will be preferred over another grammar with no contentRegex
. If the contentRegex
does not match, a grammar with no contentRegex
will be preferred over this one.The HTML classes that Pulsar uses for syntax highlighting do not correspond directly to nodes in the syntax tree. Instead, Tree-sitter grammar files specify scope mappings that specify which classes should be applied to which syntax nodes. The scopes
object controls these scope mappings. Its keys are CSS selectors that select nodes in the syntax tree. Its values can be of several different types.
Here is a simple example:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
This entry means that, in the syntax tree, any identifier
node whose parent is a call_expression
should be highlighted using three classes: syntax--entity
, syntax--name
, and syntax--function
.
Note that in this selector, we're using the immediate child combinator (>
). Arbitrary descendant selectors without this combinator (for example 'call_expression identifier'
, which would match any identifier
occurring anywhere within a call_expression
) are currently not supported.
The keys of the scopes
object can also contain multiple CSS selectors, separated by commas, similar to CSS files. The triple-quote syntax in CSON makes it convenient to write keys like this on multiple lines:
scopes:
+ '''
+ function_declaration > identifier,
+ call_expression > identifier,
+ call_expression > field_expression > field_identifier
+ ''': 'entity.name.function'
+
You can use the :nth-child
pseudo-class to select nodes based on their order within their parent. For example, this example selects identifier
nodes which are the fourth (zero-indexed) child of a singleton_method
node.
scopes:
+ 'singleton_method > identifier:nth-child(3)': 'entity.name.function'
+
Finally, you can use double-quoted strings in the selectors to select anonymous tokens in the syntax tree, like (
and :
. See the Tree-sitter documentation for more information about named vs anonymous tokens.
scopes:
+ '''
+ "*",
+ "/",
+ "+",
+ "-"
+ ''': 'keyword.operator'
+
You can also apply different classes to a syntax node based on its text. Here are some examples:
scopes:
+
+ # Apply the classes `syntax--builtin` and `syntax--variable` to all
+ # `identifier` nodes whose text is `require`.
+ 'identifier': {exact: 'require', scopes: 'builtin.variable'},
+
+ # Apply the classes `syntax--type` and `syntax--integer` to all
+ # `primitive_type` nodes whose text starts with `int` or `uint`.
+ 'primitive_type': {match: /^u?int/, scopes: 'type.integer'},
+
+ # Apply the classes `syntax--builtin`, `syntax--class`, and
+ # `syntax--name` to `constant` nodes with the text `Array`,
+ # `Hash` and `String`. For all other `constant` nodes, just
+ # apply the classes `syntax--class` and `syntax--name`.
+ 'constant': [
+ {match: '^(Array|Hash|String)$', scopes: 'builtin.class.name'},
+ 'class.name'
+ ]
+
In total there are four types of values that can be associated with selectors in scopes
:
syntax--
and applied to the selected node.exact
and scopes
- If the node's text equals the exact
string, the scopes
string will be used as described above.match
and scopes
- If the node's text matches the match
regex pattern, the scopes
string will be used as described above.If multiple selectors in the scopes
object match a node, the node's classes will be decided based on the most specific selector. Note that the exact
and match
rules do not affect specificity, so you may need to supply the same exact
or match
rules for multiple selectors to ensure that they take precedence over other selectors. You can use the same selector multiple times in a scope mapping, within different comma-separated keys:
scopes:
+ 'call_expression > identifier': 'entity.name.function'
+
+ # If we did not include the second selector here, then this rule
+ # would not apply to identifiers inside of call_expressions,
+ # because the selector `call_expression > identifier` is more
+ # specific than the selector `identifier`.
+ 'identifier, call_expression > identifier': [
+ {exact: 'require', scopes: 'builtin.variable'},
+ {match: '^[A-Z]', scopes: 'constructor'},
+ ]
+
Sometimes, a source file can contain code written in several different languages. Tree-sitter grammars support this situation using a two-part process called language injection. First, an 'outer' language must define an injection point - a set of syntax nodes whose text can be parsed using a different language, along with some logic for guessing the name of the other language that should be used. Second, an 'inner' language must define an injectionRegex
- a regex pattern that will be tested against the language name provided by the injection point.
For example, in JavaScript, tagged template literals sometimes contain code written in a different language, and the name of the language is often used in the 'tag' function, as shown in this example:
// HTML in a template literal
+const htmlContent = html`<div>Hello ${name}</div>`;
+
The tree-sitter-javascript
parser parses this tagged template literal as a call_expression
with two children: an identifier
and a template_literal
:
(call_expression
+ (identifier)
+ (template_literal
+ (interpolation
+ (identifier))))
+
Here is an injection point that would allow syntax highlighting inside of template literals:
atom.grammars.addInjectionPoint("source.js", {
+ type: "call_expression",
+
+ language(callExpression) {
+ const { firstChild } = callExpression;
+ if (firstChild.type === "identifier") {
+ return firstChild.text;
+ }
+ },
+
+ content(callExpression) {
+ const { lastChild } = callExpression;
+ if (lastChild.type === "template_string") {
+ return lastChild;
+ }
+ },
+});
+
The language
callback would then be called with every call_expression
node in the syntax tree. In the example above, it would retrieve the first child of the call_expression
, which is an identifier
with the name "html". The callback would then return the string "html".
The content
callback would then be called with the same call_expression
node and return the template_string
node within the call_expression
node.
In order to parse the HTML within the template string, the HTML grammar file would need to specify an injectionRegex
:
injectionRegex: 'html|HTML'
+
The next field in the grammar file, folds
, controls code folding. Its value is an array of fold pattern objects. Fold patterns are used to decide whether or not a syntax node can be folded, and if so, where the fold should start and end. Here are some example fold patterns:
folds: [
+
+ # All `comment` nodes are foldable. By default, the fold starts at
+ # the end of the node's first line, and ends at the beginning
+ # of the node's last line.
+ {
+ type: 'comment'
+ }
+
+ # `if_statement` nodes are foldable if they contain an anonymous
+ # "then" token and either an `elif_clause` or `else_clause` node.
+ # The fold starts at the end of the "then" token and ends at the
+ # `elif_clause` or `else_clause`.
+ {
+ type: 'if_statement',
+ start: {type: '"then"'}
+ end: {type: ['elif_clause', 'else_clause']}
+ }
+
+ # Any node that starts with an anonymous "(" token and ends with
+ # an anonymous ")" token is foldable. The fold starts after the
+ # "(" and ends before the ")".
+ {
+ start: {type: '"("', index: 0},
+ end: {type: '")"', index: -1}
+ }
+]
+
Fold patterns can have one or more of the following fields:
type
- A string or array of strings. In order to be foldable according to this pattern, a syntax node's type must match one of these strings.start
- An object that is used to identify a child node after which the fold should start. The object can have one or both of the following fields: type
- A string or array of strings. To start a fold, a child node's type must match one of these strings.index
- a number that's used to select a specific child according to its index. Negative values are interpreted as indices relative the last child, so that -1
means the last child.end
- An object that is used to identify a child node before which the fold should end. It has the same structure as the start
object.The last field in the grammar file, comments
, controls the behavior of Pulsar's Editor: Toggle Line Comments
command. Its value is an object with a start
field and an optional end
field. The start field is a string that should be prepended to or removed from lines in order to comment or uncomment them.
In JavaScript, it looks like this:
comments:
+ start: '// '
+
The end
field should be used for languages that only support block comments, not line comments. If present, it will be appended to or removed from the end of the last selected line in order to comment or un-comment the selection.
In CSS, it would look like this:
comments:
+ start: '/* '
+ end: ' */'
+
More examples of all of these features can be found in the Tree-sitter grammars bundled with Pulsar:
Pulsar's syntax highlighting can be powered by two types of grammars. If you're adding support for a new language, the preferred way is to create a Tree-sitter grammar. Tree-sitter grammars have better performance and provide support for more editor features, such as the Select Larger Syntax Node
command.
This section describes the Pulsar's legacy support for TextMate grammars.
TextMate grammars are supported by several popular text editors. They provide a set of regex (regular expression) patterns which are assigned scopes. These scopes are then turned into the CSS classes that you can target in syntax themes.
TextMate Grammars depend heavily on regexes, and you should be comfortable with interpreting and writing regexes before continuing. Note that Pulsar uses the Oniguruma engine, which is very similar to the PCRE or Perl regex engines. Here are some resources to help you out:
Grammar files are written in the CSON or JSON format. Whichever one you decide to use is up to you, but this tutorial will be written in CSON.
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing "Generate Package" to generate a new grammar package. Select "Package Generator: Generate Package," and you'll be asked for the path where your package will be created. Let's call ours language-flight-manual
.
Tip
Grammar packages should start with language-.
The default package template creates a lot of folders that aren't needed for grammar packages. Go ahead and delete the keymaps
, lib
, menus
, and styles
folders. Furthermore, in package.json
, remove the activationCommands
section. Now create a new folder called grammars
, and inside that a file called flight-manual.cson
. This is the main file that we will be working with - start by populating it with a boilerplate template. Now let's go over what each key means.
scopeName
is the root scope of your package. This should generally describe what language your grammar package is highlighting; for example, language-javascript
's scopeName
is source.js
and language-html
's is text.html.basic
. Name it source.flight-manual
for now.
name
is the user-friendly name that is displayed in places like the status bar or the grammar selector. Again, this name should describe what the grammar package is highlighting. Rename it to Flight Manual
.
fileTypes
is an array of filetypes that language-flight-manual
should highlight. We're interested in highlighting the Flight Manual's Markdown files, so add the md
extension to the list and remove the others.
patterns
contains the array of regex patterns that will determine how the file is tokenized.
To start, let's add a basic pattern to tokenize the words Flight Manual
whenever they show up. Your regex should look like \bFlight Manual\b
. Here's what your patterns
block should look like:
'patterns': [
+ {
+ 'match': '\\bFlight Manual\\b'
+ 'name': 'entity.other.flight-manual'
+ }
+]
+
match
is where your regex is contained, and name
is the scope name that is to be applied to the entirety of the match. More information about scope names can be found in Section 12.4 of the TextMate Manual.
Tip
All scopes should end with the portion of the root scopeName
after the leading source
or text
. In our case, all scopes should end with flight-manual
.
Note
Astute readers may have noticed that the \b
was changed to \\b
with two backslashes and not one. This is because CSON processes the regex string before handing it to Oniguruma, so all backslashes need to be escaped twice.
But what if we wanted to apply different scopes to Flight
and Manual
? This is possible by adding capture groups to the regex and then referencing those capture groups in a new capture
property. For example:
'match': '\\b(Flight) (Manual)\\b'
+'name': 'entity.other.flight-manual'
+'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+
This will assign the scope keyword.other.flight.flight-manual
to Flight
, keyword.other.manual.flight-manual
to Manual
, and entity.other.flight-manual
to the overarching Flight Manual
.
Now let's say we want to tokenize the {{#note}}
blocks that occur in Flight Manual files. Our previous two examples used match
, but one limit of match
is that it can only match single lines. {{#note}}
blocks, on the other hand, can span multiple lines. For these cases, you can use the begin
/end
keys. Once the regex in the begin
key is matched, tokenization will continue until the end
pattern is reached.
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+
Tip
Get into the habit of providing punctuation scopes early on. It's much less effort than having to go back and rewriting all your patterns to support punctuation scopes when your grammar starts to get a bit longer!
Awesome, we have our first multiline pattern! However, if you've been following along and playing around in your own .md
file, you may have noticed that Flight Manual
doesn't receive any scopes inside a note block. A begin/end block is essentially a subgrammar of its own: once it starts matching, it will only match its own subpatterns until the end pattern is reached. Since we haven't defined any subpatterns, then clearly nothing will be matched inside of a note block. Let's fix that!
'begin': '({{)(#note)(}})'
+'beginCaptures':
+ '0': # The 0 capture contains the entire match
+ 'name': 'meta.block.start.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'end': '({{)(/note)(}})'
+'endCaptures':
+ '0':
+ 'name': 'meta.block.end.flight-manual'
+ '1':
+ 'name': 'punctuation.definition.block.flight-manual'
+ '2':
+ 'name': 'keyword.note.flight-manual'
+ '3':
+ 'name': 'punctuation.definition.block.flight-manual'
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'match': '\\b(Flight) (Manual)\\b'
+ 'name': 'entity.other.flight-manual'
+ 'captures':
+ '1':
+ 'name': 'keyword.other.flight.flight-manual'
+ '2':
+ 'name': 'keyword.other.manual.flight-manual'
+ }
+]
+
There. With the patterns block, Flight Manual
should now receive the proper scopes.
At this point, note blocks are looking pretty nice, as is the Flight Manual
keyword, but the rest of the file is noticeably lacking any form of Markdown syntax highlighting. Is there a way to include the GitHub-Flavored Markdown grammar without copying and pasting everything over? This is where the include
keyword comes in. include
allows you to include other patterns, even from other grammars! language-gfm
's scopeName
is source.gfm
, so let's include that. Our patterns
block should now look like the following:
'patterns': [
+ {
+ 'include': 'source.gfm'
+ }
+ {
+ # Flight Manual pattern
+ }
+ {
+ # Note begin/end pattern
+ }
+]
+
However, including source.gfm
has led to another problem: note blocks still don't have any Markdown highlighting! The quick fix would be to add the include pattern to the note's pattern block as well, but now we're duplicating two patterns. You can imagine that as this grammar grows it'll quickly become inefficient to keep copying each new global pattern over to the note
pattern as well. Therefore, include
helpfully recognizes the special $self
scope. $self
automatically includes all the top-level patterns of the current grammar. The note
block can then be simplified to the following:
'begin': '({{)(#note)(}})'
+# beginCaptures
+'end': '({{)(/note)(}})'
+# endCaptures
+'name': 'meta.block.note.flight-manual'
+'patterns': [
+ {
+ 'include': '$self'
+ }
+]
+
There are several good resources out there that help when writing a grammar. The following is a list of some particularly useful ones (some have been linked to in the sections above as well).
first-mate
. Not necessary to write a grammar, but a good technical reference for what Pulsar is doing behind the scenes.Pulsar's interface is rendered using HTML, and it's styled via Less which is a superset of CSS. Don't worry if you haven't heard of Less before; it's just like CSS, but with a few handy extensions.
Pulsar supports two types of themes: UI and Syntax. UI themes style elements such as the tree view, the tabs, drop-down lists, and the status bar. Syntax themes style the code, gutter and other elements inside the editor view.
Themes can be installed and changed from the Settings View which you can open by selecting the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu, and clicking the "Install" or "Themes" tab on the left hand navigation.
Themes are pretty straightforward but it's still helpful to be familiar with a few things before starting:
package.json
(as covered in Pulsar package.json
). This file is used to help distribute your theme to Pulsar users.package.json
must contain a theme
key with a value of ui
or syntax
for Pulsar to recognize and load it as a theme.Let's create your first theme.
To get started, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and start typing Generate Syntax Theme
to generate a new theme package. Select Generate Syntax Theme
, and you'll be asked for the path where your theme will be created. Let's call ours motif-syntax
.
Tip
Syntax themes should end with -syntax and UI themes should end with -ui.
Pulsar will display a new window, showing the motif-syntax theme, with a default set of folders and files created for us. If you open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+, and click the "Themes" tab on the left, you'll see the "Motif" theme listed in the "Syntax Theme" drop-down. Select it from the menu to activate it, now when you open an editor you should see your new motif-syntax theme in action.
Open up styles/colors.less
to change the various color variables which have already been defined. For example, turn @red
into #f4c2c1
.
Then open styles/base.less
and modify the various selectors that have already been defined. These selectors style different parts of code in the editor such as comments, strings and the line numbers in the gutter.
As an example, let's make the .gutter
background-color
into @red
.
Reload Pulsar by pressing LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L to see the changes you made reflected in your Pulsar window. Pretty neat!
Tip
You can avoid reloading to see changes you make by opening an Pulsar window in Dev Mode. To open a Dev Mode Pulsar window run pulsar --dev .
in the terminal, or use the View > Developer > Open in Dev Mode menu. When you edit your theme, changes will instantly be reflected!
Note
It's advised to not specify a font-family
in your syntax theme because it will override the Font Family field in Pulsar's settings. If you still like to recommend a font that goes well with your theme, we suggest you do so in your README.
To create a UI theme, do the following:
pulsar --dev .
in the terminal or use the View > Developer > Open in Dev Mode menupackage.json
file-ui
, for example super-white-ui
pulsar -p link --dev
to symlink your repository to LNX/MAC: ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar
Tip
Because we used pulsar -p link --dev
in the above instructions, if you break anything you can always close Pulsar and launch Pulsar normally to force Pulsar to the default theme. This allows you to continue working on your theme even if something goes catastrophically wrong.
UI themes must provide a ui-variables.less
and Syntax themes a syntax-variables.less
file. It contains predefined variables that packages use to make sure the look and feel matches.
Here the variables with the default values:
These default values will be used as a fallback in case a theme doesn't define its own variables.
In any of your package's .less
files, you can access the theme variables by importing the ui-variables
or syntax-variables
file from Pulsar.
Your package should generally only specify structural styling, and these should come from the style guide. Your package shouldn't specify colors, padding sizes, or anything in absolute pixels. You should instead use the theme variables. If you follow this guideline, your package will look good out of the box with any theme!
Here's an example .less
file that a package can define using theme variables:
@import "ui-variables";
+
+.my-selector {
+ background-color: @base-background-color;
+ padding: @component-padding;
+}
+
@import "syntax-variables";
+
+.my-selector {
+ background-color: @syntax-background-color;
+}
+
There are a few tools to help make theme development faster and easier.
Reloading by pressing LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L after you make changes to your theme is less than ideal. Pulsar supports live updating of styles on Pulsar windows in Dev Mode.
To launch a Dev Mode window:
pulsar --dev
If you'd like to reload all the styles at any time, you can use the shortcut LNX/WIN: Alt+Ctrl+R - MAC: Alt+Cmd+Ctrl+L,
Pulsar is based on the Chromium browser and supports its Developer Tools. You can open them by selecting the View > Developer > Toggle Developer Tools menu, or by using the LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I
The dev tools allow you to inspect elements and take a look at their CSS properties.
Check out Google's extensive tutorial for a short introduction.
If you are creating an UI theme, you'll want a way to see how your theme changes affect all the components in the system. The Styleguide is a page that renders every component Pulsar supports.
To open the Styleguide, open the command palette with LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or use the shortcut LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G.
Sometimes when creating a theme (or package) things can go wrong and the editor becomes unusable. E.g. if the text and background have the same color or something gets pushed out of sight. To avoid having to open Pulsar in "normal" mode to fix the issue, it's advised to open two Pulsar windows. One for making changes and one in Dev Mode to see the changes getting applied.
Make changes on the left, see the changes getting applied in "Dev Mode" on the right.
Now if you mess up something, only the window in "Dev Mode" will be affected and you can easily correct the mistake in your "normal" window.
Once you're happy with your theme and would like to share it with other Pulsar users, it's time to publish it. 🎉
Follow the steps on the Publishing page. The example used is for the Word Count package, but publishing a theme works exactly the same.
Pulsar runs on a number of platforms and while Electron and Node take care of many of the details there are still some considerations to ensure your package works on other operating systems.
File symlinks can be used on Windows by non-Administrators by specifying 'junction' as the type (this argument is ignored on macOS & Linux).
Also consider:
fs.symlink
insteadcom1
-com9
, lpt1
-lpt9
, con
, nul
, aux
and prn
(regardless of extension, e.g. prn.txt
is disallowed)/my\ test
"c:\my test"
\
although some tools and PowerShell allow /
too/
You can dynamically find out what your platform uses with path.sep
or better yet use the node path library functions such as join
and normalize
which automatically take care of this.
Windows supports up to 250 characters for a path - avoid deeply nested directory structures
URL parsing routines should not be used on file paths. While they initially look like a relative path it will fail in a number of scenarios on all platforms.
?
as query string, #
as a fragment identifierIf you need to use a path for a URL use the file: protocol with an absolute path instead to ensure drive letters and slashes are appropriately addressed, e.g. file:///c|/test/pic.png
fs.stat
on directoriesThe fs.stat
function does not return the size of the contents of a directory but rather the allocation size of the directory itself. This returns 0 on Windows and 1024 on macOS and so should not be relied upon.
path.relative
can't traverse drivespath.relative
can be used to calculate a relative path to traverse between any two given paths.c:\
and d:\
Creation and deletion operations may take a few milliseconds to complete. If you need to remove many files and folders consider RimRAF which has built-in retry logic for this.
LF
CRLF
autocrlf
set which automatically converts between the twoIf you are writing specs that use text file fixtures consider that this will interfere with file lengths, hash codes and direct text comparisons. It will also change the Atom selection length by 1 character per line.
If you have spec fixtures that are text files you may want to tell Git to force LF, CRLF or not convert them by specifying the paths in .gitattributes
e.g.
spec/fixtures/always-crlf.txt eol=crlf
+spec/fixtures/always-lf.txt eol=lf
+spec/fixtures/leave-as-is.txt -text
+
Pulsar provides several tools to help you understand unexpected behavior and debug problems. This guide describes some of those tools and a few approaches to help you debug and provide more helpful information when submitting issues:
You might be running into an issue which was already fixed in a more recent version of Pulsar than the one you're using.
If you're using a released version, check which version of Pulsar you're using:
$ pulsar --version
+> Pulsar : 1.63.0-dev
+> Electron: 12.2.3
+> Chrome : 89.0.4389.128
+> Node : 14.16.0
+
You can find the latest releases on the Pulsar Website, follow the links for either the latest release or Cirrus CI version. Make sure to mention which version when logging an issue.
If you're building Pulsar from source, pull down the latest version of master and re-build. Make sure that if logging an issue you include the latest commit hash you built from.
A large part of Pulsar's functionality comes from packages you can install. Pulsar will also execute the code in your init script on startup. In some cases, these packages and the code in the init script might be causing unexpected behavior, problems, or performance issues.
To determine if that is happening, start Pulsar from the terminal in safe mode:
$ pulsar --safe
+
This starts Pulsar, but does not load packages from LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\packages
or %USERPROFILE%\.pulsar\dev\packages
. and disables loading of your init script. If you can no longer reproduce the problem in safe mode, it's likely it was caused by one of the packages or the init script.
If removing or commenting out all content from the init script and starting Pulsar normally still produces the error, then try figuring out which package is causing trouble. Start Pulsar normally again and open the Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+,. Since the Settings View allows you to disable each installed package, you can disable packages one by one until you can no longer reproduce the issue. Restart Pulsar or reload Pulsar with LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L. after you disable each package to make sure it's completely gone.
When you find the problematic package, you can disable or uninstall the package. We strongly recommend creating an issue on the package's GitHub repository.
Pulsar saves a number of things about your environment when you exit in order to restore Pulsar to the same configuration when you next launch the program. In some cases the state that gets saved can be something undesirable that prevents Pulsar from working properly. In these cases, you may want to clear the state that Pulsar has saved.
DANGER
Clearing the saved state permanently destroys any state that Pulsar has saved across all projects. This includes unsaved changes to files you may have been editing in all projects. This is a destructive action.
Clearing the saved state can be done by opening a terminal and executing:
$ pulsar --clear-window-state
+
In some cases, you may want to reset Pulsar to "factory defaults", in other words clear all of your configuration and remove all packages. This can easily be done by opening a terminal and executing:
Once that is complete, you can launch Pulsar as normal. Everything will be just as if you first installed Pulsar.
Tip
The command given above doesn't delete the old configuration, just puts it somewhere that Pulsar can't find it. If there are pieces of the old configuration you want to retrieve, you can find them in the LNX/MAC: ~/.pulsar-backup
- WIN: %USERPROFILE%\.pulsar-backup
directory.
If you develop or contribute to Pulsar packages, there may be left-over packages linked to your LNX/MAC: ~/.pulsar/packages
or ~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\packages
or %USERPROFILE%\.pulsar\dev\packages
. directories. You can use the pulsar -p links
command to list all linked packages:
$ pulsar -p links
+> /Users/pulsy/.pulsar/dev/packages (0)
+> └── (no links)
+> /Users/pulsy/.pulsar/packages (1)
+> └── color-picker -> /Users/pulsy/github/color-picker
+
You can remove links using the pulsar -p unlink
command:
$ pulsar -p unlink color-picker
+> Unlinking /Users/pulsy/.pulsar/packages/color-picker ✓
+
See pulsar -p links --help
and pulsar -p unlink --help
for more information on these commands.
Tip
You can also use pulsar -p unlink --all
to easily unlink all packages and themes.
If you have packages installed that use native Node modules, when you upgrade to a new version of Pulsar, they might need to be rebuilt. Pulsar detects this and through the incompatible-packages package displays an indicator in the status bar when this happens.
If you see this indicator, click it and follow the instructions.
In some cases, unexpected behavior might be caused by settings in Pulsar or in one of the packages.
Open Pulsar's Settings View with LNX/WIN: Ctrl+, - MAC: Cmd+,, the LNX: Edit > Preferences - MAC: Pulsar > Preferences - WIN: File > Preferences menu option, or the Settings View: Open
command from the Command Palette.
Check Pulsar's settings in the Settings View, there's a description of most configuration options in the Basic Customization section. For example, if you want Pulsar to hide the invisible symbols representing whitespace characters, disable the "Show Invisibles" option.
Some of these options are also available on a per-language basis which means that they may be different for specific languages, for example JavaScript or Python. To check the per-language settings, open the settings for the language package under the Packages tab in the Settings View, for example the language-javascript or language-python package.
Since Pulsar ships with a set of packages and you can also install additional packages yourself, check the list of packages and their settings. For instance, if you'd like to get rid of the vertical line in the middle of the editor, disable the Wrap Guide package. And if you don't like it when Pulsar strips trailing whitespace or ensures that there's a single trailing newline in the file, you can configure that in the whitespace package's settings.
You might have defined some custom styles, keymaps or snippets in one of your configuration files. In some situations, these personal hacks might be causing the unexpected behavior you're observing so try clearing those files and restarting Pulsar.
If a command is not executing when you press a key combination or the wrong command is executing, there might be an issue with the keybinding for that combination. Pulsar ships with the Keybinding Resolver, a neat package which helps you understand what key Pulsar saw you press and the command that was triggered because of it.
Show the keybinding resolver with LNX/WIN: Ctrl+. - MAC: Cmd+., or with Keybinding Resolver: Show
from the Command palette. With the Keybinding Resolver shown, press a key combination:
The Keybinding Resolver shows you a list of keybindings that exist for the key combination, where each item in the list has the following:
The keybindings are listed in two colors. All the keybindings that are matched but not executed are shown in gray. The one that is executed, if any, is shown in green. If the command you wanted to trigger isn't listed, then a keybinding for that command hasn't been loaded.
If multiple keybindings are matched, Pulsar determines which keybinding will be executed based on the specificity of the selectors and the order in which they were loaded. If the command you wanted to trigger is listed in the Keybinding Resolver, but wasn't the one that was executed, this is normally explained by one of two causes:
The key combination was not used in the context defined by the keybinding's selector
For example, you can't trigger the keybinding for the tree-view:add-file
command if the Tree View is not focused.
There is another keybinding that took precedence
This often happens when you install a package which defines keybindings that conflict with existing keybindings. If the package's keybindings have selectors with higher specificity or were loaded later, they'll have priority over existing ones.
Pulsar loads core Pulsar keybindings and package keybindings first, and user-defined keybindings last. Since user-defined keybindings are loaded last, you can use your keymap.cson
file to tweak the keybindings and sort out problems like these. See the Keymaps in Depth section for more information.
If you notice that a package's keybindings are taking precedence over core Pulsar keybindings, it might be a good idea to report the issue on that package's GitHub repository. You can contact Pulsar maintainers on Pulsar's github discussions.
You can determine which fonts are being used to render a specific piece of text by using the Developer Tools. To open the Developer Tools press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I. Once the Developer Tools are open, click the "Elements" tab. Use the standard tools for finding the element containing the text you want to check. Once you have selected the element, you can click the "Computed" tab in the styles pane and scroll to the bottom. The list of fonts being used will be shown there:
When an unexpected error occurs in Pulsar, you will normally see a red notification which provides details about the error and allows you to create an issue on the right repository:
Not all errors are logged with a notification so if you suspect you're experiencing an error but there's no notification, you can also look for errors in the developer tools Console tab. To access the Console tab, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I to open developer tools and then click the Console tab:
If there are multiple errors, you can scroll down to the bottom of the panel to see the most recent error. Or while reproducing an error, you can right click in the Console tab panel, select Clear console
to remove all Console output, and then reproduce the error to see what errors are logged to the Console tab.
Note
When running in Dev Mode, the developer tools are automatically shown with the error logged in the Console tab.
If Pulsar is taking a long time to start, you can use the Timecop package to get insight into where Pulsar spends time while loading.
Timecop displays the following information:
If a specific package has high load or activation times, you might consider reporting an issue to the maintainers. You can also disable the package to potentially improve future startup times.
If you're experiencing performance problems in a particular situation, your Issue reports will be more valuable if you include a saved profile from Chrome's CPU profiler that gives some insight into what is slow.
To run a profile, open the Developer Tools with LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I. From there:
Once that is done, then perform the slow action to capture a recording. When finished, click "Stop". Switch to the "Chart" view, and a graph of the recorded actions will appear. You can save and post the profile data by clicking "Save" next to the profile's name in the left panel.
To learn more, check out the Chrome documentation on CPU profiling.
If the time for loading the window looks high, you can create a CPU profile for that period using the --profile-startup
command line flag when starting Pulsar:
$ pulsar --profile-startup .
+
This will automatically capture a CPU profile as Pulsar is loading and open the Developer Tools once Pulsar loads. From there:
You can then include the startup profile in any issue you report.
If you are having issues installing a package using pulsar -p install
, this could be because the package has dependencies on libraries that contain native code. This means you will need to have a C++ compiler and Python installed to be able to install it. You can run pulsar -p install --check
to see if the Pulsar package manager can build native code on your machine.
Check out the pre-requisites in the build instructions for your platform for more details.
If you encounter flickering or other rendering issues, you can stop Pulsar from using your Graphics Processing Unit (GPU) with the --disable-gpu
Chromium flag to see if the fault lies with your GPU:
$ pulsar --disable-gpu
+
Chromium (and thus Pulsar) normally uses the GPU to accelerate drawing parts of the interface. --disable-gpu
tells Pulsar to not even attempt to do this, and just use the CPU for rendering everything. This means that the parts of the interface that would normally be accelerated using the GPU will instead take slightly longer and render on the CPU. This likely won't make a noticeable difference, but does slightly increase the battery usage on portable devices as the CPU has to work harder to do the things the GPU is optimized for.
Two other Chromium flags that are useful for debugging are --enable-gpu-rasterization
and --force-gpu-rasterization
:
$ pulsar --enable-gpu-rasterization --force-gpu-rasterization
+
--enable-gpu-rasterization
allows other commands to determine how a layer tile (graphics) should be drawn and --force-gpu-rasterization
determines that the Skia GPU backend should be used for drawing layer tiles (only valid with GPU accelerated compositing).
Be sure to use Chromium flags at the end of the terminal call if you want to use other Pulsar flags as they will not be executed after the Chromium flags e.g.:
$ pulsar --safe --enable-gpu-rasterization --force-gpu-rasterization
+
You will first want to build and run Pulsar from source.
Once you have a local copy of Pulsar cloned and built, you can then run Pulsar in Development Mode. But first, if you cloned Pulsar to somewhere other than LNX/MAC: ~/github/pulsar
- WIN: %USERPROFILE%\github\pulsar
you will need to set the ATOM_DEV_RESOURCE_PATH
environment variable to point to the folder in which you cloned Pulsar. To run Pulsar in Dev Mode, use the --dev
parameter from the terminal:
$ pulsar --dev <path-to-open>
+
There are a couple benefits of running Pulsar in Dev Mode:
ATOM_DEV_RESOURCE_PATH
environment variable is set correctly, Pulsar is run using the source code from your local pulsar-edit/pulsar
repository. This means you don't have to rebuild after every change, just restart Pulsar 👍~/.pulsar/dev/packages
- WIN: %USERPROFILE%\.pulsar\dev\packages
are loaded instead of packages of the same name normally loaded from other locations. This means that you can have development versions of packages you use loaded but easily go back to the stable versions by launching without Dev Mode.window:reload
) to see changes to those.In order to run Pulsar Core tests from the terminal, first be certain to set the ATOM_DEV_RESOURCE_PATH
environment variable as mentioned above and then:
$ cd <path-to-your-local-pulsar-repo>
+$ pulsar --test spec
+
Packages have the ability to handle special URIs triggered from the system; for example, a package named my-package
can register itself to handle any URI starting with atom://my-package/
.
WARNING
Handling URIs triggered from other applications, like a web browser, is a powerful tool, but also one that can be jarring. You should shape your package's user experience to handle this well. In general, you should avoid taking direct action on behalf of a user. For example, a URI handler that immediately installs a package is too invasive, but a URI handler that shows the package's pane in the settings view is useful. A URI handler that begins to clone a repo is overly aggressive, but a URI handler that prompts the user to clone a repo is okay.
Any package with a URI handler that we feel violates this guideline is subject to removal from the Pulsar package registry at our discretion.
package.json
The first step to handling URIs from your package is to modify its package.json
file. You should add a new key called uriHandler
, and its value should be an object.
The uriHandler
object must contain a key called method
with a string value that tells Pulsar which method in your package to call when a URI needs to be handled. The object can optionally include a key called deferActivation
which can be set to the boolean false
to prevent Pulsar from deferring activation of your package — see more below.
For example, if we want our package my-package
to handle URIs with a method on our package's main module called handleURI
, we could add the following to our package.json
:
"uriHandler": {
+ "method": "handleURI"
+}
+
Now that we've told Pulsar that we want our package to handle URIs beginning with atom://my-package/
via our handleURI
method, we need to actually write this method. Pulsar passes two arguments to your URI handler method; the first one is the fully-parsed URI plus query string, parsed with Node's url.parse(uri, true)
. The second argument is the raw, string URI; this is normally not needed since the first argument gives you structured information about the URI.
Here's a sample package, written in JavaScript, that handles URIs with the package.json
configuration we saw above.
export default {
+ activate() {
+ // normal activation code here
+ },
+
+ handleURI(parsedUri) {
+ console.log(parsedUri);
+ },
+};
+
When Pulsar handles, for example, the URI atom://my-package/my/test/url?value=42&other=false
, the package would log out something like the following:
{
+ protocol: 'atom:',
+ slashes: true,
+ auth: null,
+ host: 'my-package',
+ port: null,
+ hostname: 'my-package',
+ hash: null,
+ search: '?value=true&other=false',
+ query: { value: '42', other: 'false' },
+ pathname: '/my/test/url',
+ path: '/my/test/url?value=true&other=false',
+ href: 'atom://my-package/my/test/url?value=true&other=false'
+}
+
Notice that the query string arguments are available in the query
property, but are strings — you'll have to convert to other native types yourself.
For performance reasons, adding a uriHandler
entry to your package's package.json
will enable deferred activation. This means that Pulsar will not activate your package until it has a URI for it to handle — it will then activate your package and then immediately call the URI handler method. If you want to disable the deferred activation, ensuring your package is activated upon startup, you can add "deferActivation": false
to the URI handler config. For example,
"uriHandler": {
+ "method": "handleURI",
+ "deferActivation": false
+}
+
Before doing this, make sure your package actually needs to be activated immediately — disabling deferred activation means Pulsar takes longer to start since it has to activate all packages without deferred activation.
Because URI handling is different across operating systems and distributions, there is no built-in URI handler support for Pulsar on Linux. If you want to configure URI handling on your system yourself, then you should configure atom:
protocol URI's to trigger Pulsar with the --uri-handler
flag; for example, the URI atom://test/uri
should launch Atom via atom --uri-handler atom://test/uri
.
Pulsar provides a core URI to handle opening files with the syntax atom://core/open/file?filename=<filepath>&line=<line>&column=<col>
Pulsar comes bundled with the Octicons 4.4.0 icon set. Use them to add icons to your packages.
NOTE: Some older icons from version
2.1.2
are still kept for backwards compatibility.
In the Styleguide under the "Icons" section you'll find all the Octicons that are available.
Octicons can be added with simple CSS classes in your markup. Prefix the icon names with icon icon-
.
As an example, to add a monitor icon (device-desktop
), use the icon icon-device-desktop
classes:
<span class="icon icon-device-desktop"></span>
+
Octicons look best with a font-size
of 16px
. It's already used as the default, so you don't need to worry about it. In case you prefer a different icon size, try to use multiples of 16 (32px
, 48px
etc.) for the sharpest result. Sizes in between are ok too, but might look a bit blurry for icons with straight lines.
Although icons can make your UI visually appealing, when used without a text label, it can be hard to guess its meaning. In cases where space for a text label is insufficient, consider adding a tooltip that appears on hover. Or a more subtle title="label"
attribute would help as well.
Originally, each of Atom's core packages resided in a separate repository. In 2018, in an effort to streamline the development of Atom by reducing overhead, the Atom team consolidated many core Atom packages into the atom/atom repository. For example, the one-light-ui package was originally maintained in the atom/one-light-ui repository, but was moved to the packages/one-light-ui
directory in the main repository.
The Pulsar team has continued this trend and has move even more packages into the core, particularly default language packages. A list of these packages moved can be found in this document.
If you forked one of the core packages before it was moved into the atom/atom or pulsar-edit/pulsar repository, and you want to continue merging upstream changes into your fork, please follow the steps below.
For the sake of this guide, let's assume that you forked the pulsar-edit/one-light-ui repository, renamed your fork to one-light-ui-plus
, and made some customizations.
Navigate to your local clone of your fork:
$ cd path/to/your/fork
+
Add the pulsar-edit/pulsar repository as a git remote:
$ git remote add upstream https://github.com/pulsar-edit/pulsar.git
+
Tip
Follow these steps each time you want to merge upstream changes into your fork.
Fetch the latest changes from the pulsar-edit/pulsar repository:
$ git fetch upstream
+
Identify recent changes to the core package. For example, if you're maintaining a fork of the one-light-ui package, then you'll want to identify recent changes in the packages/one-light-ui
directory:
$ git log --oneline upstream/master -- packages/one-light-ui
+f884f6de8 [themes] Rename A[a]tom -> P[p]ulsar
+0db3190f4 Additional rebranding where needed
+234adb874 Remove deprecated code strings
+...
+
Look through the log and identify the commits that you want to merge into your fork.
For each commit that you want to bring into your fork, use [git format-patch
][https://git-scm.com/docs/git-format-patch] in conjunction with [git am
][https://git-scm.com/docs/git-am]. For example, to merge commit f884f6de8
into your fork:
$ git format-patch -1 --stdout f884f6de8 | git am -p3
+
Repeat this step for each commit that you want to merge into your fork.
We saw in our Word Count package how we could show information in a modal panel. However, panels aren't the only way to extend Pulsar's UI—you can also add items to the workspace. These items can be dragged to new locations (for example, one of the docks on the edges of the window), and Pulsar will restore them the next time you open the project.
For this package, we'll define a workspace item that tells us some information about our active text editor. The final package can be viewed at https://github.com/pulsar-edit/active-editor-info.
To begin, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P to bring up the Command Palette. Type "generate package" and select the Package Generator: Generate Package
command, just as we did in the section on package generation. Enter active-editor-info
as the name of the package.
Now let's edit the package files to show our view in a workspace item instead of a modal panel. The way we do this is by registering an opener with Pulsar. Openers are just functions that accept a URI and return a view (if it's a URI that the opener knows about). When you call atom.workspace.open()
, Pulsar will go through all of its openers until it finds one that can handle the URI you passed.
Let's open lib/active-editor-info.js
and edit our activate()
method to register an opener:
"use babel";
+
+import ActiveEditorInfoView from "./active-editor-info-view";
+import { CompositeDisposable, Disposable } from "atom";
+
+export default {
+ subscriptions: null,
+
+ activate(state) {
+ this.subscriptions = new CompositeDisposable(
+ // Add an opener for our view.
+ atom.workspace.addOpener((uri) => {
+ if (uri === "atom://active-editor-info") {
+ return new ActiveEditorInfoView();
+ }
+ }),
+
+ // Register command that toggles this view
+ atom.commands.add("atom-workspace", {
+ "active-editor-info:toggle": () => this.toggle(),
+ }),
+
+ // Destroy any ActiveEditorInfoViews when the package is deactivated.
+ new Disposable(() => {
+ atom.workspace.getPaneItems().forEach((item) => {
+ if (item instanceof ActiveEditorInfoView) {
+ item.destroy();
+ }
+ });
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ toggle() {
+ console.log("Toggle it!");
+ },
+};
+
You'll notice we also removed the activeEditorInfoView
property and the serialize()
method. That's because, with workspace items, it's possible to have more than one instance of a given view. Since each instance can have its own state, each should do its own serialization instead of relying on a package-level serialize()
method. We'll come back to that later.
You probably also noticed that our toggle()
implementation just logs the text "Toggle it!" to the console. Let's make it actually toggle our view:
toggle() {
+ atom.workspace.toggle('atom://active-editor-info');
+ }
+
Pulsar uses the same view abstractions everywhere, so we can almost use the generated ActiveEditorInfoView class as-is. We just need to add two small methods:
getTitle() {
+ // Used by Pulsar for tab text
+ return 'Active Editor Info';
+ }
+
+ getURI() {
+ // Used by Pulsar to identify the view when toggling.
+ return 'atom://active-editor-info';
+ }
+
Now reload the window and run the Active Editor Info: Toggle
command from the command palette! Our view will appear in a new tab in the center of the workspace. If you want, you can drag it into one of the docks. Toggling it again will then hide that dock. If you close the tab and run the toggle command again, it will appear in the last place you had it.
Note
We've repeated the same URI three times now. That's okay, but it's probably a good idea to define the URL in one place and then import it from that module wherever you need it.
The purpose of our view is to show information about the active text editor, so it doesn't really make sense to show our item in the center of the workspace (where the text editor will be). Let's add some methods to our view class to influence where its opened:
getDefaultLocation() {
+ // This location will be used if the user hasn't overridden it by dragging the item elsewhere.
+ // Valid values are "left", "right", "bottom", and "center" (the default).
+ return 'right';
+ }
+
+ getAllowedLocations() {
+ // The locations into which the item can be moved.
+ return ['left', 'right', 'bottom'];
+ }
+
Now our item will appear in the right dock initially and users will only be able to drag it to one of the other docks.
Now that we have our view all wired up, let's update it to show some information about the active text editor. Add this to the constructor:
this.subscriptions = atom.workspace
+ .getCenter()
+ .observeActivePaneItem((item) => {
+ if (!atom.workspace.isTextEditor(item)) {
+ message.innerText = "Open a file to see important information about it.";
+ return;
+ }
+ message.innerHTML = `
+ <h2>${item.getFileName() || "untitled"}</h2>
+ <ul>
+ <li><b>Soft Wrap:</b> ${item.softWrapped}</li>
+ <li><b>Tab Length:</b> ${item.getTabLength()}</li>
+ <li><b>Encoding:</b> ${item.getEncoding()}</li>
+ <li><b>Line Count:</b> ${item.getLineCount()}</li>
+ </ul>
+ `;
+ });
+
Now whenever you open a text editor in the center, the view will update with some information about it.
WARNING
We use a template string here because it's simple and we have a lot of control over what's going into it, but this could easily result in the insertion of unwanted HTML if you're not careful. Sanitize your input and use the DOM API or a templating system when doing this for real.
Also, don't forget to clean up the subscription in the destroy()
method:
destroy() {
+ this.element.remove();
+ this.subscriptions.dispose();
+}
+
If you were to reload Atom now, you'd see that our item had disappeared. That's because we haven't told Pulsar how to serialize it yet. Let's do that now.
The first step is to implement a serialize()
method on our ActiveEditorInfoView class. Atom will call the serialize()
method on every item in the workspace periodically to save its state.
serialize() {
+ return {
+ // This is used to look up the deserializer function. It can be any string, but it needs to be
+ // unique across all packages!
+ deserializer: 'active-editor-info/ActiveEditorInfoView'
+ };
+ }
+
Note
All of our view's state is derived from the active text editor so we only need the deserializer
field. If we had other state that we wanted to preserve across reloads, we would just add things to the object we're returning. Just make sure that they're JSON serializable!
Next we need to register a deserializer function that Atom can use to recreate the real object when it starts up. The best way to do that is to add a "deserializers" object to our package.json
file:
{
+ "name": "active-editor-info",
+ ...
+ "deserializers": {
+ "active-editor-info/ActiveEditorInfoView": "deserializeActiveEditorInfoView"
+ }
+}
+
Notice that the key ("active-editor-info/ActiveEditorInfoView"
) matches the string we used in our serialize()
method above. The value ("deserializeActiveEditorInfoView"
) refers to a function in our main module, which we still need to add. Go back to active-editor-info.js
and do that now:
deserializeActiveEditorInfoView(serialized) {
+ return new ActiveEditorInfoView();
+ }
+
The value returned from our serialize()
method will be passed to this function. Since our serialized object didn't include any state, we can just return a new ActiveEditorInfoView instance.
Reload Pulsar and toggle the view with the Active Editor Info: Toggle
command. Then reload Pulsar again. Your view should be just where you left it!
In this section, we've made a toggleable workspace item whose placement can be controlled by the user. This could be helpful when creating all sorts of visual tools for working with code!
Now that we have our first package written, let's go through examples of other types of packages we can make. This section will guide you though creating a simple command that replaces the selected text with ascii art. When you run our new command with the word "cool" selected, it will be replaced with:
o888 + ooooooo ooooooo ooooooo 888 + 888 888 888 888 888 888 888 + 888 888 888 888 888 888 + 88ooo888 88ooo88 88ooo88 o888o + +
This should demonstrate how to do basic text manipulation in the current text buffer and how to deal with selections.
The final package can be viewed at https://github.com/pulsar-edit/ascii-art.
To begin, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P to bring up the Command Palette. Type "generate package" and select the "Package Generator: Generate Package" command, just as we did in the section on package generation. Enter ascii-art
as the name of the package.
Now let's edit the package files to make our ASCII Art package do something interesting. Since this package doesn't need any UI, we can remove all view-related code so go ahead and delete lib/ascii-art-view.js
, spec/ascii-art-view-spec.js
, and styles/
.
Next, open up lib/ascii-art.js
and remove all view code, so it looks like this:
const { CompositeDisposable } = require("atom");
+
+module.exports = {
+ subscriptions: null,
+
+ activate() {
+ this.subscriptions = new CompositeDisposable();
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "ascii-art:convert": () => this.convert(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.subscriptions.dispose();
+ },
+
+ convert() {
+ console.log("Convert text!");
+ },
+};
+
Now let's add a command. You should namespace your commands with the package name followed by a :
and then the name of the command. As you can see in the code, we called our command ascii-art:convert
and we will define it to call the convert()
method when it's executed.
So far, that will simply log to the console. Let's start by making it insert something into the text buffer.
convert() {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ editor.insertText('Hello, World!')
+ }
+}
+
As in Counting Words, we're using atom.workspace.getActiveTextEditor()
to get the object that represents the active text editor. If this convert()
method is called when not focused on a text editor, nothing will happen.
Next we insert a string into the current text editor with the insertText()
method. This will insert the text wherever the cursor currently is in the current editor. If there are selections, it will replace all selections with the "Hello, World!" text.
Before we can trigger ascii-art:convert
, we need to load the latest code for our package by reloading the window. Run the command "Window: Reload" from the Command Palette or by pressing LNX/WIN: Ctrl+Shift+F5 - MAC: Alt+Cmd+Ctrl+L
Now open the Command Palette and search for the Ascii Art: Convert
command. But it's not there! To fix this, open package.json
and find the property called activationCommands
. Activation commands make Pulsar launch faster by allowing Pulsar to delay a package's activation until it's needed. So remove the existing command and use ascii-art:convert
in activationCommands
:
"activationCommands": {
+ "atom-workspace": "ascii-art:convert"
+}
+
First, reload the window by running the command "Window: Reload" from the command palette. Now when you run the Ascii Art: Convert
command it will insert "Hello, World!" into the active editor, if any.
Now let's add a key binding to trigger the ascii-art:convert
command. Open keymaps/ascii-art.json
and add a key binding linking Alt+Ctrl+A to the ascii-art:convert
command. You can delete the pre-existing key binding since you won't need it anymore.
When finished, the file should look like this:
{
+ "atom-text-editor": {
+ "ctrl-alt-a": "ascii-art:convert"
+ }
+}
+
+
Now reload the window and verify that the key binding works.
WARNING
The Pulsar keymap system is case-sensitive. This means that there is a distinction between a
and A
when creating keybindings. a
means that you want to trigger the keybinding when you press A. But A
means that you want to trigger the keybinding when you press Shift+A. You can also write shift-a
when you want to trigger the keybinding when you press Shift+A.
We strongly recommend always using lowercase and explicitly spelling out when you want to include Shift in your keybindings.
Now we need to convert the selected text to ASCII art. To do this we will use the figlet Node module from npm. Open package.json
and add the latest version of figlet to the dependencies:
"dependencies": {
+ "figlet": "1.0.8"
+}
+
After saving the file, run the command Update Package Dependencies: Update
from the Command Palette. This will install the package's node module dependencies, only figlet in this case. You will need to run Update Package Dependencies: Update
whenever you update the dependencies field in your package.json
file.
If for some reason this doesn't work, you'll see a message saying "Failed to update package dependencies" and you will find a new npm-debug.log
file in your directory. That file should give you some idea as to what went wrong.
Now require the figlet node module in lib/ascii-art.js
and instead of inserting "Hello, World!", convert the selected text to ASCII art.
convert () {
+ const editor = atom.workspace.getActiveTextEditor()
+ if (editor) {
+ const selection = editor.getSelectedText()
+
+ const figlet = require('figlet')
+ const font = 'o8'
+ figlet(selection, {font}, function (error, art) {
+ if (error) {
+ console.error(error)
+ } else {
+ editor.insertText(`\n${art}\n`)
+ }
+ })
+ }
+}
+
Now reload the editor, select some text in an editor window and press Alt+Ctrl+A. It should be replaced with a ridiculous ASCII art version instead.
There are a couple of new things in this example we should look at quickly. The first is the editor.getSelectedText()
which, as you might guess, returns the text that is currently selected.
We then call the Figlet code to convert that into something else and replace the current selection with it with the editor.insertText()
call.
In this section, we've made a UI-less package that takes selected text and replaces it with a processed version. This could be helpful in creating linters or checkers for your code.
Let's get started by writing a very simple package and looking at some of the tools needed to develop one effectively. We'll start by writing a package that tells you how many words are in the current buffer and display it in a small modal window.
The simplest way to start a package is to use the built-in package generator that ships with Pulsar. As you might expect by now, this generator is itself a separate package implemented in package-generator.
You can run the generator by invoking the command palette and searching for "Generate Package". A dialog will appear asking you to name your new project. Name it your-name-word-count
. Pulsar will then create that directory and fill it out with a skeleton project and link it into your LNX/MAC: ~/.pulsar/packages
- WIN: %USERPROFILE%\.pulsar\packages
directory so it's loaded when you launch your editor next time.
Note
You may encounter a situation where your package is not loaded. That is because a new package using the same name as an actual package hosted on pulsar-edit.dev (e.g. "wordcount" and "word-count") is not being loaded as you expected. If you follow our suggestion above of using the your-name-word-count
package name, you should be safe 😀
You can see that Pulsar has created about a dozen files that make up the package. Let's take a look at each of them to get an idea of how a package is structured, then we can modify them to get our word count functionality.
The basic package layout is as follows:
my-package/
+├─ grammars/
+├─ keymaps/
+├─ lib/
+├─ menus/
+├─ spec/
+├─ snippets/
+├─ styles/
+├─ index.js
+└─ package.json
+
Not every package will have (or need) all of these directories and the package generator doesn't create snippets
or grammars
. Let's see what some of these are so we can start messing with them.
package.json
Similar to Node modules, Pulsar packages contain a package.json
file in their top-level directory. This file contains metadata about the package, such as the path to its "main" module, library dependencies, and manifests specifying the order in which its resources should be loaded.
In addition to some of the regular Node package.json
keys available, Pulsar package.json
files have their own additions.
main
: the path to the JavaScript file that's the entry point to your package. If this is missing, Pulsar will default to looking for an index.js
or index.coffee
.styles
: an Array of Strings identifying the order of the style sheets your package needs to load. If not specified, style sheets in the styles
directory are added alphabetically.keymaps
: an Array of Strings identifying the order of the key mappings your package needs to load. If not specified, mappings in the keymaps
directory are added alphabetically.menus
: an Array of Strings identifying the order of the menu mappings your package needs to load. If not specified, mappings in the menus
directory are added alphabetically.snippets
: an Array of Strings identifying the order of the snippets your package needs to load. If not specified, snippets in the snippets
directory are added alphabetically.activationCommands
: an Object identifying commands that trigger your package's activation. The keys are CSS selectors, the values are Arrays of Strings identifying the command. The loading of your package is delayed until one of these events is triggered within the associated scope defined by the CSS selector. If not specified, the activate()
method of your main export will be called when your package is loaded.activationHooks
: an Array of Strings identifying hooks that trigger your package's activation. The loading of your package is delayed until one of these hooks are triggered. Currently, there are three activation hooks: core:loaded-shell-environment
for when Pulsar has finished loading the shell environment variablesscope.name:root-scope-used
for when a file is opened from the specified language (e.g. source.ruby:root-scope-used
)language-package-name:grammar-used
for when a specific language package is used (e.g., my-special-language-javascript:grammar-used
)workspaceOpeners
: An Array of Strings identifying URIs that trigger your package's activation. For example, say your package registers a custom opener for atom://my-custom-panel
. By including that string in workspaceOpeners
, your package will defer its activation until that URI is opened.The package.json
in the package we've just generated looks like this currently:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationCommands": {
+ "atom-workspace": "your-name-word-count:toggle"
+ },
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
If you wanted to use activationHooks, you might have:
{
+ "name": "your-name-word-count",
+ "main": "./lib/your-name-word-count",
+ "version": "0.0.0",
+ "description": "A short description of your package",
+ "activationHooks": [
+ "language-javascript:grammar-used",
+ "language-coffee-script:grammar-used"
+ ],
+ "repository": "https://github.com/pulsar-edit/your-name-word-count",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0"
+ },
+ "dependencies": {}
+}
+
One of the first things you should do is ensure that this information is filled out. The name, description, repository URL the project will be at, and the license can all be filled out immediately. The other information we'll get into more detail on as we go.
WARNING
Do not forget to update the repository URL. The one generated for you is invalid by design and will prevent you from publishing your package until updated.
If you want to extend Pulsar's behavior, your package should contain a single top-level module, which you export from whichever file is indicated by the main
key in your package.json
file. In the package we just generated, the main package file is lib/your-name-word-count.js
. The remainder of your code should be placed in the lib
directory, and required from your top-level file. If the main
key is not in your package.json
file, it will look for index.js
or index.coffee
as the main entry point.
Your package's top-level module is a singleton object that manages the lifecycle of your extensions to Pulsar. Even if your package creates ten different views and appends them to different parts of the DOM, it's all managed from your top-level object.
Your package's top-level module can implement the following basic methods:
activate(state)
: This optional method is called when your package is activated. It is passed the state data from the last time the window was serialized if your module implements the serialize()
method. Use this to do initialization work when your package is started (like setting up DOM elements or binding events). If this method returns a promise the package will be considered loading until the promise resolves (or rejects).initialize(state)
: This optional method is similar to activate()
but is called earlier. Whereas activation occurs after the workspace has been deserialized (and can therefore happen after your package's deserializers have been called), initialize()
is guaranteed to be called before everything. Use activate()
if you want to be sure that the workspace is ready; use initialize()
if you need to do some setup prior to your deserializers or view providers being invoked.serialize()
: This optional method is called when the window is shutting down, allowing you to return JSON to represent the state of your component. When the window is later restored, the data you returned is passed to your module's activate
method so you can restore your view to where the user left off.deactivate()
: This optional method is called when the window is shutting down and when the package is disabled. If your package is watching any files or holding external resources in any other way, release them here. You should also dispose of all subscriptions you're holding on to.Style sheets for your package should be placed in the styles
directory. Any style sheets in this directory will be loaded and attached to the DOM when your package is activated. Style sheets can be written as CSS or Less, but Less is recommended.
Ideally, you won't need much in the way of styling. Pulsar provides a standard set of components which define both the colors and UI elements for any package that fits into Pulsar seamlessly. You can view all of Pulsar's UI components by opening the styleguide: open the command palette LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for styleguide
, or type LNX/WIN: Ctrl+Shift+G - MAC: Cmd+Ctrl+Shift+G
If you do need special styling, try to keep only structural styles in the package style sheets. If you must specify colors and sizing, these should be taken from the active theme's ui-variables.less.
An optional styleSheets
array in your package.json
can list the style sheets by name to specify a loading order; otherwise, style sheets are loaded alphabetically.
You can provide key bindings for commonly used actions for your extension, especially if you're also adding a new command. In our new package, we have a keymap filled in for us already in the keymaps/your-name-word-count.json
file:
{
+ "atom-workspace": {
+ "ctrl-alt-o": "your-name-word-count:toggle"
+ }
+}
+
This means that if you press Alt+Ctrl+O, our package will run the your-name-word-count:toggle
command. We'll look at that code next, but if you want to change the default key mapping, you can do that in this file.
Keymaps are placed in the keymaps
subdirectory. By default, all keymaps are loaded in alphabetical order. An optional keymaps
array in your package.json
can specify which keymaps to load and in what order.
Keybindings are executed by determining which element the keypress occurred on. In the example above, the your-name-word-count:toggle
command is executed when pressing Alt+Ctrl+O on the atom-workspace
element. Because the atom-workspace
element is the parent of the entire Pulsar UI, this means the key combination will work anywhere in the application.
We'll cover more advanced keybinding stuff a bit later in Keymaps in Depth.
Menus are placed in the menus
subdirectory. This defines menu elements like what pops up when you right click a context-menu or would go in the application menu to trigger functionality in your package.
By default, all menus are loaded in alphabetical order. An optional menus
array in your package.json
can specify which menus to load and in what order.
It's recommended that you create an application menu item under the Packages menu for common actions with your package that aren't tied to a specific element. If we look in the menus/your-name-word-count.json
file that was generated for us, we'll see a section that looks like this:
+"menu": [
+ {
+ "label": "Packages",
+ "submenu": [
+ {
+ "label": "Word Count",
+ "submenu": [
+ {
+ "label": "Toggle",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+ ]
+ }
+]
+
+
This section puts a "Toggle" menu item under a menu group named "Your Name Word Count" in the "Packages" menu.
When you select that menu item, it will run the your-name-word-count:toggle
command, which we'll look at in a bit.
The menu templates you specify are merged with all other templates provided by other packages in the order which they were loaded.
It's recommended to specify a context menu item for commands that are linked to specific parts of the interface. In our menus/your-name-word-count.json
file, we can see an auto-generated section that looks like this:
"context-menu": {
+ "atom-text-editor": [
+ {
+ "label": "Toggle your-name-word-count",
+ "command": "your-name-word-count:toggle"
+ }
+ ]
+ }
+
This adds a "Toggle Word Count" menu option to the menu that pops up when you right-click in an Pulsar text editor pane.
When you click that it will again run the your-name-word-count:toggle
method in your code.
Context menus are created by determining which element was selected and then adding all of the menu items whose selectors match that element (in the order which they were loaded). The process is then repeated for the elements until reaching the top of the DOM tree.
You can also add separators and submenus to your context menus. To add a submenu, provide a submenu
key instead of a command. To add a separator, add an item with a single type: 'separator'
key/value pair. For instance, you could do something like this:
{
+ "context-menu": {
+ "atom-workspace": [
+ {
+ "label": "Text",
+ "submenu": [
+ {
+ "label": "Inspect Element",
+ "command": "core:inspect"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Selector All",
+ "command": "core:select-all"
+ },
+ {
+ "type": "separator"
+ },
+ {
+ "label": "Deleted Selected Text",
+ "command": "core:delete"
+ }
+ ]
+ }
+ ]
+ }
+}
+
Currently with the generated package we have, if we run that your-name-word-count:toggle
command through the menu or the command palette, we'll get a dialog that says "The YourNameWordCount package is Alive! It's ALIVE!".
Let's take a look at the code in our lib
directory and see what is happening.
There are two files in our lib
directory. One is the main file (lib/your-name-word-count.js
), which is pointed to in the package.json
file as the main file to execute for this package. This file handles the logic of the whole package.
The second file is a View class, lib/your-name-word-count-view.js
, which handles the UI elements of the package. Let's look at this file first, since it's pretty simple.
export default class YourNameWordCountView {
+ constructor(serializedState) {
+ // Create root element
+ this.element = document.createElement("div");
+ this.element.classList.add("your-name-word-count");
+
+ // Create message element
+ const message = document.createElement("div");
+ message.textContent = "The YourNameWordCount package is Alive! It's ALIVE!";
+ message.classList.add("message");
+ this.element.appendChild(message);
+ }
+
+ // Returns an object that can be retrieved when package is activated
+ serialize() {}
+
+ // Tear down any state and detach
+ destroy() {
+ this.element.remove();
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
Basically the only thing happening here is that when the View class is created, it creates a simple div
element and adds the your-name-word-count
class to it (so we can find or style it later) and then adds the "Your Name Word Count package is Alive!
" text to it. There is also a getElement
method which returns that div
. The serialize
and destroy
methods don't do anything and we won't have to worry about that until another example.
Notice that we're simply using the basic browser DOM methods: createElement()
and appendChild()
.
The second file we have is the main entry point to the package. Again, because it's referenced in the package.json
file. Let's take a look at that file.
import YourNameWordCountView from "./your-name-word-count-view";
+import { CompositeDisposable } from "atom";
+
+export default {
+ yourNameWordCountView: null,
+ modalPanel: null,
+ subscriptions: null,
+
+ activate(state) {
+ this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+ );
+ this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+ });
+
+ // Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+ this.subscriptions = new CompositeDisposable();
+
+ // Register command that toggles this view
+ this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+ );
+ },
+
+ deactivate() {
+ this.modalPanel.destroy();
+ this.subscriptions.dispose();
+ this.yourNameWordCountView.destroy();
+ },
+
+ serialize() {
+ return {
+ yourNameWordCountViewState: this.yourNameWordCountView.serialize(),
+ };
+ },
+
+ toggle() {
+ console.log("YourNameWordCount was toggled!");
+ return this.modalPanel.isVisible()
+ ? this.modalPanel.hide()
+ : this.modalPanel.show();
+ },
+};
+
There is a bit more going on here. First of all we can see that we are defining four methods. The only required one is activate
. The deactivate
and serialize
methods are expected by Pulsar but optional. The toggle
method is one Pulsar is not looking for, so we'll have to invoke it somewhere for it to be called, which you may recall we do both in the activationCommands
section of the package.json
file and in the action we have in the menu file.
The deactivate
method simply destroys the various class instances we've created and the serialize
method simply passes on the serialization to the View class. Nothing too exciting here.
The activate
command does a number of things. For one, it is not called automatically when Pulsar starts up, it is first called when one of the activationCommands
as defined in the package.json
file are called. In this case, activate
is only called the first time the toggle
command is called. If nobody ever invokes the menu item or hotkey, this code is never called.
This method does two things. The first is that it creates an instance of the View class we have and adds the element that it creates to a hidden modal panel in the Pulsar workspace.
this.yourNameWordCountView = new YourNameWordCountView(
+ state.yourNameWordCountViewState
+);
+this.modalPanel = atom.workspace.addModalPanel({
+ item: this.yourNameWordCountView.getElement(),
+ visible: false,
+});
+
We'll ignore the state stuff for now, since it's not important for this simple package. The rest should be fairly straightforward.
The next thing this method does is create an instance of the CompositeDisposable class so it can register all the commands that can be called from the package so other packages could subscribe to these events.
// Events subscribed to in Pulsar's system can be easily cleaned up with a CompositeDisposable
+this.subscriptions = new CompositeDisposable();
+
+// Register command that toggles this view
+this.subscriptions.add(
+ atom.commands.add("atom-workspace", {
+ "your-name-word-count:toggle": () => this.toggle(),
+ })
+);
+
Next we have the toggle
method. This method simply toggles the visibility of the modal panel that we created in the activate
method.
toggle() {
+ console.log('YourNameWordCount was toggled!');
+ return (
+ this.modalPanel.isVisible() ?
+ this.modalPanel.hide() :
+ this.modalPanel.show()
+ );
+}
+
This should be fairly simple to understand. We're looking to see if the modal element is visible and hiding or showing it depending on its current state.
So, let's review the actual flow in this package.
package.json
your-name-word-count:toggle
activate
method in your main module which sets up the UI by creating the hidden modal viewyour-name-word-count:toggle
which reveals the hidden modal viewyour-name-word-count:toggle
command againTip
Keep in mind that the flow will be slightly different if you choose not to use activationCommands
in your package.
So now that we understand what is happening, let's modify the code so that our little modal box shows us the current word count instead of static text.
We'll do this in a very simple way. When the dialog is toggled, we'll count the words right before displaying the modal. So let's do this in the toggle
command. If we add some code to count the words and ask the view to update itself, we'll have something like this:
toggle() {
+ if (this.modalPanel.isVisible()) {
+ this.modalPanel.hide();
+ } else {
+ const editor = atom.workspace.getActiveTextEditor();
+ const words = editor.getText().split(/\s+/).length;
+ this.yourNameWordCountView.setCount(words);
+ this.modalPanel.show();
+ }
+}
+
Let's look at the 3 lines we've added. First we get an instance of the current editor object (where our text to count is) by calling atom.workspace.getActiveTextEditor()
.
Next we get the number of words by calling getText()
on our new editor object, then splitting that text on whitespace with a regular expression and then getting the length of that array.
Finally, we tell our view to update the word count it displays by calling the setCount()
method on our view and then showing the modal again. Since that method doesn't yet exist, let's create it now.
We can add this code to the end of our your-name-word-count-view.js
file:
setCount(count) {
+ const displayText = `There are ${count} words.`;
+ this.element.children[0].textContent = displayText;
+}
+
Pretty simple! We take the count number that was passed in and place it into a string that we then stick into the element that our view is controlling.
Note
To see your changes, you'll need to reload the code. You can do this by reloading the window (The window:reload
command in the Command Palette). A common practice is to have two Pulsar windows, one for developing your package, and one for testing and reloading.
You'll notice a few console.log
statements in the code. One of the cool things about Pulsar being built on Chromium is that you can use some of the same debugging tools available to you that you have when doing web development.
To open up the Developer Console, press LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I or choose the menu option View > Developer > Toggle Developer Tools.
From here you can inspect objects, run code and view console output just as though you were debugging a web site.
Your package should have tests, and if they're placed in the spec
directory, they can be run by Pulsar.
Under the hood, Jasmine v1.3 executes your tests, so you can assume that any DSL available there is also available to your package.
Once you've got your test suite written, you can run it by pressing LNX/WIN: Alt+Ctrl+P - MAC: Alt+Cmd+Ctrl+P or via the View > Developer > Run Package Specs menu. Our generated package comes with an example test suite, so you can run this right now to see what happens.
You can also use the pulsar --test spec
command to run them from the command line. It prints the test output and results to the console and returns the proper status code depending on whether the tests passed or failed.
We've now generated, customized and tested our first package for Pulsar. Congratulations! Now let's go ahead and publish it so it's available to the world.
Pulsar bundles a command line utility called ppm
which we first used back in Command Line to search for and install packages via the command line. This is invoked by using the pulsar
command with the -p
or --package
option. The pulsar -p
command can also be used to publish Pulsar packages to the public registry and update them.
See more in Using PPM.
There are a few things you should double check before publishing:
package.json
file has name
, description
, and repository
fields.package.json
name
is URL Safe, as in it's not an emoji or special character.package.json
file has a version
field with a value of "0.0.0"
.package.json
version
field is Semver V2 compliant.package.json
file has an engines
field that contains an entry for atom
such as: "engines": {"atom": ">=1.0.0 <2.0.0"}
.README.md
file at the root.repository
URL in the package.json
file is the same as the URL of your repository.Before you publish a package it is a good idea to check ahead of time if a package with the same name has already been published to the Pulsar Package Repository. You can do that by visiting https://web.pulsar-edit.dev/packages/your-package-name
to see if the package already exists. If it does, update your package's name to something that is available before proceeding.
Now let's review what the pulsar -p publish
command does:
version
field in the package.json
file and commits it.Now run the following commands to publish your package:
$ cd path-to-your-package
+$ pulsar -p publish minor
+
If this is the first package you are publishing, the pulsar -p publish
command may prompt you for your GitHub username and password. If you have two-factor authentication enabled, use a personal access token in lieu of a password. This is required to publish and you only need to enter this information the first time you publish. The credentials are stored securely in your keychain once you login.
Your package is now published and available on Pulsar Package Repository. Head on over to https://web.pulsar-edit.dev/packages/your-package-name
to see your package's page.
With pulsar -p publish
, you can bump the version and publish by using
$ pulsar -p publish <version-type>
+
where version-type
can be major
, minor
and patch
.
i.e. to bump a package from v1.0.0 to v1.1.0:
$ pulsar -p publish minor
+
Check out semantic versioning to learn more about best practices for versioning your package releases.
You can also run pulsar -p help publish
to see all the available options and pulsar -p help
to see all the other available commands.
If you finished this chapter, you should be an Pulsar-hacking master. We've discussed how you should work with JavaScript and CoffeeScript, and how to put it to good use in creating packages. You should also be able to do this in your own created theme now.
Even when something goes wrong, you should be able to debug this easily. But also fewer things should go wrong, because you are capable of writing great specs for Pulsar.
In the next chapter, we’ll go into more of a deep dive on individual internal APIs and systems of Pulsar, even looking at some Pulsar source to see how things are really getting done.
Info
The default init file for Pulsar has been changed from the previous CoffeeScript init.coffee
file used by Atom to JavaScript. The CoffeeScript file will still work but should you wish to reference the specific version of this document for it then you should look at the Atom Archive.
When Pulsar finishes loading, it will evaluate init.js
in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory, giving you a chance to run JavaScript code to make customizations. Code in this file has full access to Pulsar's API. If customizations become extensive, consider creating a package, which we will cover in Package: Word Count.
You can open the init.js
file in an editor from the LNX: Atom > Init Script - MAC: File > Init Script - WIN: Edit > Init Script menu.
For example, if you have the Audio Beep configuration setting enabled, you could add the following code to your init.js
file to have Pulsar greet you with an audio beep every time it loads:
atom.beep();
+
Because init.js
provides access to Pulsar's API, you can use it to implement useful commands without creating a new package or extending an existing one. Here's a command which uses the Selection API and Clipboard API to construct a Markdown link from the selected text and the clipboard contents as the URL:
atom.commands.add("atom-text-editor", "markdown:paste-as-link", () => {
+ let clipboardText, editor, selection;
+ if (!(editor = atom.workspace.getActiveTextEditor())) {
+ return;
+ }
+ selection = editor.getLastSelection();
+ clipboardText = atom.clipboard.read();
+ return selection.insertText(
+ "[" + selection.getText() + "](" + clipboardText + ")"
+ );
+});
+
Now, reload Pulsar and use the Command Palette to execute the new command, Markdown: Paste As Link
, by name. And if you'd like to trigger the command via a keyboard shortcut, you can define a keybinding for the command.
To begin, there are a few things we'll assume you know, at least to some degree. Since all of Pulsar is implemented using web technologies, we have to assume you know web technologies such as JavaScript and CSS. Specifically, we'll be using Less, which is a preprocessor for CSS.
While much of Pulsar has been converted to JavaScript, a lot of older code is still implemented in CoffeeScript but the process of "decaffeination" is ongoing, to continue this conversion feel free to read more. Additionally, Pulsar's default configuration language is CSON, which is based on CoffeeScript. If you don't know CoffeeScript, but you are familiar with JavaScript, you shouldn't have too much trouble. Here is an example of some simple CoffeeScript code:
MyPackageView = require './my-package-view'
+
+module.exports =
+ myPackageView: null
+
+ activate: (state) ->
+ @myPackageView = new MyPackageView(state.myPackageViewState)
+
+ deactivate: ->
+ @myPackageView.destroy()
+
+ serialize: ->
+ myPackageViewState: @myPackageView.serialize()
+
We'll go over examples like this in a bit, but this is what the language looks like. Just about everything you can do with CoffeeScript in Pulsar is also doable in JavaScript. You can brush up on CoffeeScript at coffeescript.org.
Less is an even simpler transition from CSS. It adds a number of useful things like variables and functions to CSS. You can learn about Less at lesscss.org. Our usage of Less won't get too complex in this book however, so as long as you know basic CSS you should be fine.
ppm
is used for installing and managing Pulsar's packages in much the same way that apm
did on Atom. However at this point in the project there are a few hoops you have to jump through to get it to work correctly.
After following the build instructions you will find the ppm
binary at pulsar/ppm/bin/apm
but by default Pulsar will be looking in the wrong place. There will also be issues relating to the Electron version which will prevent install from the package backend. To solve this a couple of environmental variables need to be exported.
You can now use the binary to link or install packages.
For example to install the ide-java
package from source:
# clone the repository and cd into it
+git clone https://github.com/pulsar-edit/ide-java
+cd ide-java
+
+# from the directory where you are running pulsar source code
+<pulsar source>/ppm/bin/apm link
+
We've looked at and written a few specs through the examples already. Now it's time to take a closer look at the spec framework itself. How exactly do you write tests in Pulsar?
Pulsar uses Jasmine as its spec framework. Any new functionality should have specs to guard against regressions.
Pulsar specs and package specs are added to their respective spec
directory. The example below creates a spec for Pulsar core.
Spec files must end with -spec
so add sample-spec.js
to the spec
directory.
describe
MethodsThe describe
method takes two arguments, a description and a function. If the description explains a behavior it typically begins with when
; if it is more like a unit test it begins with the method name.
describe("when a test is written", function () {
+ // contents
+});
+
or
describe("Editor::moveUp", function () {
+ // contents
+});
+
it
MethodsThe it
method also takes two arguments, a description and a function. Try and make the description flow with the it
method. For example, a description of "this should work" doesn't read well as "it this should work". But a description of "should work" sounds great as "it should work".
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ // Expectations
+ });
+});
+
The best way to learn about expectations is to read the Jasmine documentation about them. Below is a simple example.
describe("when a test is written", function () {
+ it("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
In addition to the Jasmine's built-in matchers, Pulsar includes the following:
toBeInstanceOf
matcher is for the instanceof
operatortoHaveLength
matcher compares against the .length
propertytoExistOnDisk
matcher checks if the file exists in the filesystemtoHaveFocus
matcher checks if the element currently has focustoShow
matcher tests if the element is visible in the domThese are defined in spec/spec-helper.js.
Writing Asynchronous specs can be tricky at first. Some examples.
Working with promises is rather easy in Pulsar. You can use our waitsForPromise
function.
describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(function () {
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"));
+ });
+ });
+});
+
This method can be used in the describe
, it
, beforeEach
and afterEach
functions.
describe("when we open a file", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("c.coffee"));
+ });
+
+ it("should be opened in an editor", function () {
+ expect(atom.workspace.getActiveTextEditor().getPath()).toContain(
+ "c.coffee"
+ );
+ });
+});
+
If you need to wait for multiple promises use a new waitsForPromise
function for each promise. (Caution: Without beforeEach
this example will fail!)
describe("waiting for the packages to load", function () {
+ beforeEach(function () {
+ waitsForPromise(() => atom.workspace.open("sample.js"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tabs"));
+
+ waitsForPromise(() => atom.packages.activatePackage("tree-view"));
+ });
+
+ it("should have waited long enough", function () {
+ expect(atom.packages.isPackageActive("tabs")).toBe(true);
+ expect(atom.packages.isPackageActive("tree-view")).toBe(true);
+ });
+});
+
waitsForPromise
can take an additional object argument before the function. The object can have the following properties:
shouldReject
Whether the promise should reject or resolve (default: false
)timeout
The amount of time (in ms) to wait for the promise to be resolved or rejected (default: process.env.CI ? 60000 : 5000
)label
The label to display if promise times out (default: 'promise to be resolved or rejected'
)describe("when we open a file", function () {
+ it("should be opened in an editor", function () {
+ waitsForPromise(
+ {
+ shouldReject: false,
+ timeout: 5000,
+ label: "promise to be resolved or rejected",
+ },
+ () =>
+ atom.workspace
+ .open("c.coffee")
+ .then((editor) => expect(editor.getPath()).toContain("c.coffee"))
+ );
+ });
+});
+
Specs for asynchronous functions can be done using the waitsFor
and runs
functions. A simple example.
describe("fs.readdir(path, cb)", function () {
+ it("is async", function () {
+ const spy = jasmine.createSpy("fs.readdirSpy");
+ fs.readdir("/tmp/example", spy);
+
+ waitsFor(() => spy.callCount > 0);
+
+ runs(function () {
+ const exp = [null, ["example.coffee"]];
+
+ expect(spy.mostRecentCall.args).toEqual(exp);
+ expect(spy).toHaveBeenCalledWith(null, ["example.coffee"]);
+ });
+ });
+});
+
For a more detailed documentation on asynchronous tests please visit the Jasmine documentation.
Most of the time you'll want to run specs by triggering the window:run-package-specs
command. This command is not only to run package specs, it can also be used to run Pulsar core specs when working on Pulsar itself. This will run all the specs in the current project's spec
directory.
To run a limited subset of specs use the fdescribe
or fit
methods. You can use those to focus a single spec or several specs. Modified from the example above, focusing an individual spec looks like this:
describe("when a test is written", function () {
+ fit("has some expectations that should pass", function () {
+ expect("apples").toEqual("apples");
+ expect("oranges").not.toEqual("apples");
+ });
+});
+
It is now easy to run the specs in a CI environment like Travis and AppVeyor. See the Travis CI For Your Packages and AppVeyor CI For Your Packages posts for more details.
To run tests on the command line, run Pulsar with the --test
flag followed by one or more paths to test files or directories. You can also specify a --timeout
option, which will force-terminate your tests after a certain number of seconds have passed.
pulsar --test --timeout 60 ./test/test-1.js ./test/test-2.js
+
By default, package tests are run with Jasmine 1.3, which is outdated but can't be changed for compatibility reasons. You can specify your own custom test runner by including an atomTestRunner
field in your package.json
. Pulsar will require whatever module you specify in this field, so you can use a relative path or the name of a module in your package's dependencies.
Your test runner module must export a single function, which Pulsar will call within a new window to run your package's tests. Your function will be called with the following parameters:
testPaths
An array of paths to tests to run. Could be paths to files or directories.buildAtomEnvironment
A function that can be called to construct an instance of the atom
global. No atom
global will be explicitly assigned, but you can assign one in your runner if desired. This function should be called with the following parameters: applicationDelegate
An object responsible for Pulsar's interaction with the browser process and host OS. Use buildDefaultApplicationDelegate
for a default instance. You can override specific methods on this object to prevent or test these interactions.window
A window global.document
A document global.configDirPath
A path to the configuration directory (usually ~/.pulsar
).enablePersistence
A boolean indicating whether the Pulsar environment should save or load state from the file system. You probably want this to be false
.buildDefaultApplicationDelegate
A function that builds a default instance of the application delegate, suitable to be passed as the applicationDelegate
parameter to buildAtomEnvironment
.logFile
An optional path to a log file to which test output should be logged.headless
A boolean indicating whether or not the tests are being run from the command line via pulsar --test
.legacyTestRunner
This function can be invoked to run the legacy Jasmine runner, giving your package a chance to transition to a new test runner while maintaining a subset of its tests in the old environment.Your function should return a promise that resolves to an exit code when your tests are finished running. This exit code will be returned when running your tests via the command line.
Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
If you have any issues then please feel free to ask for help from the Pulsar Team or the wider community via any of our Community areas
If you think you have found a bug then please have a look through our existing issues and if you can't find anything then please create a new bug report.
The binary is likely from before macOS binaries started being signed. A up-to-date and signed binary may be downloaded from the download page. The unsigned binary can be made to run by running xattr -cr /Applications/Pulsar.app/
in the terminal. See here for more information.
You may need to launch the application with the argument --no-sandbox
to get around this issue. This is something under investigation.
The binary is likely from before macOS binaries started being signed. A up-to-date and signed binary may be downloaded from the download page. The unsigned binary can be made to run by running xattr -cr /Applications/Pulsar.app/
in the terminal. See here for more information.
You may need to launch the application with the argument --no-sandbox
to get around this issue. This is something under investigation.
If you have any issues then please feel free to ask for help from the Pulsar Team or the wider community via any of our Community areas
If you think you have found a bug then please have a look through our existing issues and if you can't find anything then please create a new bug report.
Under Construction
This document is under construction, please check back soon for updates. Please see our community links and feel free to ask for assistance or inquire as to the status of this document.
This section is all about how to start using Pulsar, such as how to install it and how to use it for basic text editing.
There are a lot of text editors out there; why should you spend your time learning about and using Pulsar? Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Pulsar to build Pulsar, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Pulsar is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local sub-processes was a non-starter.
For this reason, we didn't build Pulsar as a traditional web application. Instead, Pulsar is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Pulsar window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Pulsar, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Pulsar on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Pulsar as a great replacement for Atom but we can't do it without your support going forward, since we know that we can't achieve our vision for Pulsar alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
The entire Pulsar editor is free and open source and available under our Organizational repositories.
To get started with Pulsar, we'll need to get it on your system. This section will go over installing Pulsar on your system, as well as the basics of how to build it from source.
Installing Pulsar should be fairly simple. Generally, you can go to pulsar-edit.dev and you should see a download button.
Simply select your operating system (if not opened automatically) and architecture (where necessary) and choose the type of download you require.
The button or buttons should be specific to your platform, and the download package should be easily installable. However, let's go over them here in a bit of detail.
You should consider updating Pulsar periodically for the latest improvements to the software. Additionally, When Pulsar receives hotfixes for security vulnerabilities you will want to update your version of Pulsar as soon as possible.
Currently Pulsar does not support automatic updates. What this means is that new versions will have to be obtained via the Pulsar Downloads here on our website. This is something on our roadmap to change as soon as possible.
If you have installed Pulsar via a package manager then you should use the instructions provided by that package manager for updating your installation.
Pulsar stores configuration and state in a .pulsar
directory usually located in your home directory (%userprofile%
on Windows). You can however run Pulsar in portable mode where both the app and the configuration are stored together such as on a removable storage device.
To setup Pulsar in portable mode download the relevant package and extract it to your removable storage.
.pulsar
directory must be writeable.pulsar
directory to your portable device.pulsar
directory - just create a subdirectory called electronUserData
inside .pulsar
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)The Hacking the Core section of the launch manual covers instructions on how to clone and build the source code if you prefer that option.
If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ pulsar -p config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure ppm
to use it by running:
$ pulsar -p config set https-proxy <YOUR_PROXY_ADDRESS>
+
You can run pulsar -p config get https-proxy
to verify it has been set correctly.
Now that Pulsar is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Pulsar for the first time, you should get a screen that looks like this:
This is the Pulsar welcome screen and gives you a pretty good starting point for how to get started with the editor.
You can find definitions for all the various terms that we use throughout the manual in our Glossary.
In that welcome screen, we are introduced to probably the most important command in Pulsar, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like LNX: Ctrl+Shift+P - MAC: Cmd+Shift+P - WIN: Ctrl+Shift+P to demonstrate how to run a command and tabbed sections where necessary where instructions for different platforms may differ.
If you have customized your Pulsar keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Pulsar. Instead of clicking around all the application menus to look for something, you can press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut key strokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Pulsar has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Pulsar Packages.
To open the Settings View, you can:
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Pulsar. Pulsar ships with 4 different UI themes, dark and light variants of the Pulsar and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
There are also dozens of themes on the Pulsar Package Repository that you can choose from if you want something different. We will cover customizing a theme in Style Tweaks and creating your own theme in Creating a Theme.
You can use the Settings View to specify your whitespace and wrapping preferences.
Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the Tab key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.
The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.
In Basic Customization we will see how to set different wrap preferences for different types of files (for example, if you want to wrap Markdown files but not other files).
Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Pulsar. You can do it by choosing File > Open from the menu bar or by pressing
LNX/WIN: Ctrl+O - MAC: Cmd+O
to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
Another way to open a file in Pulsar is from the command line using the pulsar
command.
Note
Install Shell Commands on macOS
The Pulsar menu has an item named "Install Shell Commands" which installs the pulsar
and ppm
commands if Pulsar wasn't able to install them itself on a macOS system.
On Linux commands are installed automatically as a part of Pulsar's installation process. Windows requires the path to be exposed manually by the user at this time.
You can run the pulsar
command with one or more file paths to open up those files in Pulsar.
$ pulsar --help
+> Pulsar Editor v1.100.0
+
+> Usage: pulsar [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Pulsar window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off pulsar [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+getting-started/sections/pulsar-basics.md:130:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ pulsar getting-started/sections/pulsar-basics.md:130
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+getting-started/sections/pulsar-basics.md.md:150:722
+$ pulsar getting-started/sections/pulsar-basics.md:150:722
+
Editing a file is pretty straightforward. You can click around and scroll with your mouse and type to change the content. There is no special editing mode or key commands. If you prefer editors with modes or more complex key commands, you should take a look at the Pulsar Package Repository. There are a lot of packages that emulate popular styles.
You can open any number of directories from the command line by passing their paths to the pulsar
command line tool. For example, you could run the command pulsar ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Pulsar with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
Note
Pulsar Packages
Like many parts of Pulsar, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Pulsar by default. Packages that are bundled with Pulsar are referred to as Core packages. Ones that aren't bundled with Pulsar are referred to as Community packages.
You can find the source code to the Tree View on GitHub at https://github.com/pulsar-edit/tree-view.
This is one of the interesting things about Pulsar. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.
Once you have a project open in Pulsar, you can easily find and open any file within that project.
The fuzzy finder uses the core.ignoredNames
, fuzzy-finder.ignoredNames
and core.excludeVCSIgnoredPaths
configuration settings to filter out files and folders that will not be shown. If you have a project with tons of files you don't want it to search through, you can add patterns or paths to either of these config settings or your standard .gitignore
files. We'll learn more about config settings in Global Configuration Settings, but for now you can easily set these in the Settings View under Core Settings.
Both core.ignoredNames
and fuzzy-finder.ignoredNames
are interpreted as glob patterns as implemented by the minimatch Node module.
Tip
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
You should now have a basic understanding of what Pulsar is and what you want to do with it. You should also have it installed on your system and be able to use it for the most basic text editing operations.
Now you're ready to start digging into the fun stuff.
To get started with Pulsar, we'll need to get it on your system. This section will go over installing Pulsar on your system, as well as the basics of how to build it from source.
Installing Pulsar should be fairly simple. Generally, you can go to pulsar-edit.dev and you should see a download button.
Simply select your operating system (if not opened automatically) and architecture (where necessary) and choose the type of download you require.
The button or buttons should be specific to your platform, and the download package should be easily installable. However, let's go over them here in a bit of detail.
You should consider updating Pulsar periodically for the latest improvements to the software. Additionally, When Pulsar receives hotfixes for security vulnerabilities you will want to update your version of Pulsar as soon as possible.
Currently Pulsar does not support automatic updates. What this means is that new versions will have to be obtained via the Pulsar Downloads here on our website. This is something on our roadmap to change as soon as possible.
If you have installed Pulsar via a package manager then you should use the instructions provided by that package manager for updating your installation.
Pulsar stores configuration and state in a .pulsar
directory usually located in your home directory (%userprofile%
on Windows). You can however run Pulsar in portable mode where both the app and the configuration are stored together such as on a removable storage device.
To setup Pulsar in portable mode download the relevant package and extract it to your removable storage.
.pulsar
directory must be writeable.pulsar
directory to your portable device.pulsar
directory - just create a subdirectory called electronUserData
inside .pulsar
ATOM_HOME
environment variable to point wherever you want (you can write a .sh or .cmd script to temporarily set it and launch it from that)The Hacking the Core section of the launch manual covers instructions on how to clone and build the source code if you prefer that option.
If you are behind a firewall and seeing SSL errors when installing packages you can disable strict SSL by running:
$ pulsar -p config set strict-ssl false
+
If you are using a HTTP(S) proxy you can configure ppm
to use it by running:
$ pulsar -p config set https-proxy <YOUR_PROXY_ADDRESS>
+
You can run pulsar -p config get https-proxy
to verify it has been set correctly.
Now that Pulsar is installed on your system, let's fire it up, configure it and get acquainted with the editor.
When you launch Pulsar for the first time, you should get a screen that looks like this:
This is the Pulsar welcome screen and gives you a pretty good starting point for how to get started with the editor.
You can find definitions for all the various terms that we use throughout the manual in our Glossary.
In that welcome screen, we are introduced to probably the most important command in Pulsar, the Command Palette. If you press Cmd+Shift+PCtrl+Shift+P while focused in an editor pane, the command palette will pop up.
Note:
Throughout the book, we will use shortcut keybindings like LNX: Ctrl+Shift+P - MAC: Cmd+Shift+P - WIN: Ctrl+Shift+P to demonstrate how to run a command and tabbed sections where necessary where instructions for different platforms may differ.
If you have customized your Pulsar keymap, you can always see the keybinding you have mapped in the Command Palette or the Keybindings tab in the Settings View.
This search-driven menu can do just about any major task that is possible in Pulsar. Instead of clicking around all the application menus to look for something, you can press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P and search for the command.
Not only can you see and quickly search through thousands of possible commands, but you can also see if there is a keybinding associated with it. This is great because it means you can guess your way to doing interesting things while also learning the shortcut key strokes for doing it.
For the rest of the book, we will try to be clear as to the text you can search for in the Command Palette in addition to the keybinding for different commands.
Pulsar has a number of settings and preferences you can modify in the Settings View.
This includes things like changing the theme, specifying how to handle wrapping, font settings, tab size, scroll speed and much more. You can also use this screen to install new packages and themes, which we'll cover in Pulsar Packages.
To open the Settings View, you can:
settings-view:open
in the Command PaletteThe Settings View also lets you change the themes for Pulsar. Pulsar ships with 4 different UI themes, dark and light variants of the Pulsar and One theme, as well as 8 different syntax themes. You can modify the active theme by clicking on the Themes tab in the sidebar of the Settings View, or you can install new themes by clicking the Install tab.
The UI themes control the style of UI elements like the tabs and the tree view, while the syntax themes control the syntax highlighting of text you load into the editor. To change the syntax or UI theme, simply pick something different in the appropriate dropdown list.
There are also dozens of themes on the Pulsar Package Repository that you can choose from if you want something different. We will cover customizing a theme in Style Tweaks and creating your own theme in Creating a Theme.
You can use the Settings View to specify your whitespace and wrapping preferences.
Enabling "Soft Tabs" will insert spaces instead of actual tab characters when you press the Tab key and the "Tab Length" setting specifies how many spaces to insert when you do so, or how many spaces are used to represent a tab if "Soft Tabs" is disabled.
The "Soft Wrap" option will wrap lines that are too long to fit in your current window. If soft wrapping is disabled, the lines will simply run off the side of the screen and you will have to scroll the window to see the rest of the content. If "Soft Wrap At Preferred Line Length" is toggled, the lines will wrap at 80 characters instead of the end of the screen. You can also change the default line length to a value other than 80 on this screen.
In Basic Customization we will see how to set different wrap preferences for different types of files (for example, if you want to wrap Markdown files but not other files).
Now that your editor is looking and acting how you want, let's start opening up and editing files. This is a text editor after all, right?
There are several ways to open a file in Pulsar. You can do it by choosing File > Open from the menu bar or by pressing
LNX/WIN: Ctrl+O - MAC: Cmd+O
to choose a file from the standard dialog.
This is useful for opening a file that is not contained in the project you're currently in (more on that next), or if you're starting from a new window for some reason.
Another way to open a file in Pulsar is from the command line using the pulsar
command.
Note
Install Shell Commands on macOS
The Pulsar menu has an item named "Install Shell Commands" which installs the pulsar
and ppm
commands if Pulsar wasn't able to install them itself on a macOS system.
On Linux commands are installed automatically as a part of Pulsar's installation process. Windows requires the path to be exposed manually by the user at this time.
You can run the pulsar
command with one or more file paths to open up those files in Pulsar.
$ pulsar --help
+> Pulsar Editor v1.100.0
+
+> Usage: pulsar [options] [path ...]
+
+> One or more paths to files or folders may be specified. If there is an
+> existing Pulsar window that contains all of the given folders, the paths
+> will be opened in that window. Otherwise, they will be opened in a new
+> window.
+
+> ...
+
This is a great tool if you're used to the terminal or you work from the terminal a lot. Just fire off pulsar [files]
and you're ready to start editing. You can even open a file at a certain line (and optionally column) so the cursor will be positioned exactly where you want. For example, you may search some keyword in a repository to find the line you want to edit:
$ git grep -n 'Opening a File$'
+getting-started/sections/pulsar-basics.md:130:##### Opening a File
+
and then jump to the beginning of that line by appending a colon and the line number to the file path:
$ pulsar getting-started/sections/pulsar-basics.md:130
+
Sometimes you may want the cursor to jump to the exact column position of the searched keyword. Just append another colon plus the column number:
$ git grep -n --column 'Windows Explorer'
+getting-started/sections/pulsar-basics.md.md:150:722
+$ pulsar getting-started/sections/pulsar-basics.md:150:722
+
Editing a file is pretty straightforward. You can click around and scroll with your mouse and type to change the content. There is no special editing mode or key commands. If you prefer editors with modes or more complex key commands, you should take a look at the Pulsar Package Repository. There are a lot of packages that emulate popular styles.
You can open any number of directories from the command line by passing their paths to the pulsar
command line tool. For example, you could run the command pulsar ./hopes ./dreams
to open both the hopes
and the dreams
directories at the same time.
When you open Pulsar with one or more directories, you will automatically get a Tree View on the side of your window.
The Tree View allows you to explore and modify the file and directory structure of your project. You can open, rename, delete and create new files from this view.
Note
Pulsar Packages
Like many parts of Pulsar, the Tree View is not built directly into the editor, but is its own standalone package that is shipped with Pulsar by default. Packages that are bundled with Pulsar are referred to as Core packages. Ones that aren't bundled with Pulsar are referred to as Community packages.
You can find the source code to the Tree View on GitHub at https://github.com/pulsar-edit/tree-view.
This is one of the interesting things about Pulsar. Many of its core features are actually just packages implemented the same way you would implement any other functionality. This means that if you don't like the Tree View for example, you could write your own implementation of that functionality and replace it entirely.
Once you have a project open in Pulsar, you can easily find and open any file within that project.
The fuzzy finder uses the core.ignoredNames
, fuzzy-finder.ignoredNames
and core.excludeVCSIgnoredPaths
configuration settings to filter out files and folders that will not be shown. If you have a project with tons of files you don't want it to search through, you can add patterns or paths to either of these config settings or your standard .gitignore
files. We'll learn more about config settings in Global Configuration Settings, but for now you can easily set these in the Settings View under Core Settings.
Both core.ignoredNames
and fuzzy-finder.ignoredNames
are interpreted as glob patterns as implemented by the minimatch Node module.
Tip
Configuration Setting Notation
Sometimes you'll see us refer to configuration settings all spelled out like "Ignored Names in Core Settings". Other times you'll see us use the shorthand name like core.ignoredNames
. Both of these refer to the same thing. The shorthand is the package name, then a dot .
, followed by the "camel-cased" name of the setting.
If you have a phrase you want to camel-case, follow these steps:
So "Ignored Names" becomes "ignoredNames".
You should now have a basic understanding of what Pulsar is and what you want to do with it. You should also have it installed on your system and be able to use it for the most basic text editing operations.
Now you're ready to start digging into the fun stuff.
There are a lot of text editors out there; why should you spend your time learning about and using Pulsar? Editors like Sublime and TextMate offer convenience but only limited extensibility. On the other end of the spectrum, Emacs and Vim offer extreme flexibility, but they aren't very approachable and can only be customized with special-purpose scripting languages.
We think we can do better. Our goal is a zero-compromise combination of hackability and usability: an editor that will be welcoming to an elementary school student on their first day learning to code, but also a tool they won't outgrow as they develop into seasoned hackers.
As we've used Pulsar to build Pulsar, what began as an experiment has gradually matured into a tool we can't live without. On the surface, Pulsar is the modern desktop text editor you've come to expect. Pop the hood, however, and you'll discover a system begging to be hacked on.
The web is not without its faults, but two decades of development has forged it into an incredibly malleable and powerful platform. So when we set out to write a text editor that we ourselves would want to extend, web technology was the obvious choice. But first, we had to free it from its chains.
Web browsers are great for browsing web pages, but writing code is a specialized activity that warrants dedicated tools. More importantly, the browser severely restricts access to the local system for security reasons, and for us, a text editor that couldn't write files or run local sub-processes was a non-starter.
For this reason, we didn't build Pulsar as a traditional web application. Instead, Pulsar is a specialized variant of Chromium designed to be a text editor rather than a web browser. Every Pulsar window is essentially a locally-rendered web page.
All the APIs available to a typical Node.js application are also available to the code running in each window's JavaScript context. This hybrid provides a unique client-side development experience.
Since everything is local, you don't have to worry about asset pipelines, script concatenation, and asynchronous module definitions. If you want to load some code, just require it at the top of your file. Node's module system makes it easy to break the system down into lots of small, focused packages.
Interacting with native code is also really simple. For example, we wrote a wrapper around the Oniguruma regular expression engine for our TextMate grammar support. In a browser, that would have required adventures with NaCl or Esprima. Node integration made it easy.
In addition to the Node APIs, we also expose APIs for native dialogs, adding application and context menu items, manipulating the window dimensions, etc.
Another great benefit, that comes with writing code for Pulsar, is the guarantee that it's running on the newest version of Chromium. That means we can ignore issues like browser compatibility and polyfills. We can use all the web's shiny features of tomorrow, today.
For example, the layout of our workspace and panes is based on flexbox. It's an emerging standard and has gone through a lot of change since we started using it, but none of that mattered as long as it worked.
With the entire industry pushing web technology forward, we're confident that we're building Pulsar on fertile ground. Native UI technologies come and go, but the web is a standard that becomes more capable and ubiquitous with every passing year. We're excited to dig deeper into its toolbox.
We see Pulsar as a great replacement for Atom but we can't do it without your support going forward, since we know that we can't achieve our vision for Pulsar alone. As Emacs and Vim have demonstrated over the past three decades, if you want to build a thriving, long-lasting community around a text editor, it has to be open source.
The entire Pulsar editor is free and open source and available under our Organizational repositories.
Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
First we'll start with the Pulsar package system. As we mentioned previously, Pulsar itself is a very basic core of functionality that ships with a number of useful packages that add new features like the Tree View and the Settings View.
In fact, there are more than 80 packages that comprise all of the functionality that is available in Pulsar by default. For example, the Welcome screen that you see when you first start Pulsar, the spell checker, the themes and the Fuzzy Finder are all packages that are separately maintained and all use the same APIs that you have access to, as we'll see in great detail in Hacking the Core.
This means that packages can be incredibly powerful and can change everything from the very look and feel of the entire interface to the basic operation of even core functionality.
In order to install a new package, you can use the Install tab in the now familiar Settings View. Open up the Settings View using LNX/WIN: Ctrl+, - MAC: Cmd+, click on the "Install" tab and type your search query into the box under Install Packages.
The packages listed here have been published to https://web.pulsar-edit.dev which is the official registry for Pulsar packages. Searching on the Settings View will go to the Pulsar package registry and pull in anything that matches your search terms.
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Pulsar, it will show up in the Settings View under the "Packages" tab, along with all the pre-installed packages that come with Pulsar. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Pulsar will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Pulsar from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on pulsar-edit.dev, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
Clicking on "Install" will install the theme and make it available in the Theme dropdowns as we saw in Changing the Theme.
You can also install packages or themes from the command line using ppm (Pulsar Package Manager). This is used by running pulsar -p <commmand>
or pulsar --package <command>
.
Tip
Check that you have ppm available by running the following command in your terminal:
$ pulsar -p help install
+
You should see a message print out with details about the pulsar -p install
command.
If you do not, see the Installing Pulsar section for instructions on how to install the pulsar
command for your system.
You can install packages by using the pulsar -p install
command:
pulsar -p install <package_name>
to install the latest version.pulsar -p install <package_name>@<package_version>
to install a specific version.For example pulsar -p install minimap@4.40.0
installs the 4.40.0
release of the minimap package.
You can also use ppm to find new packages to install. If you run pulsar -p search
, you can search the package registry for a search term.
$ pulsar -p search linter
+> Search Results For 'linter' (30)
+> ├── linter A Base Linter with Cow Powers (9863242 downloads, 4757 stars)
+> ├── linter-ui-default Default UI for the Linter package (7755748 downloads, 1201 stars)
+> ├── linter-eslint Lint JavaScript on the fly, using ESLint (v7 or older) (2418043 downloads, 1660 stars)
+> ├── linter-jshint Linter plugin for JavaScript, using jshint (1202044 downloads, 1271 stars)
+> ├── linter-gcc Lint C and C++ source files using gcc / g++ (863989 downloads, 194 stars)
+> ...
+> ├── linter-shellcheck Lint Bash on the fly, using shellcheck (136938 downloads, 280 stars)
+> └── linter-rust Lint Rust-files, using rustc and/or cargo (132550 downloads, 91 stars)
+
You can use pulsar -p view
to see more information about a specific package.
$ pulsar -p view linter
+> linter
+> ├── 3.4.0
+> ├── https://github.com/steelbrain/linter
+> ├── A Base Linter with Cow Powers
+> ├── 9863242 downloads
+> └── 4757 stars
+>
+> Run `pulsar -p install linter` to install this package.
+
By default ppm will be using the Pulsar Package Repository. However you can also use it to install from other locations which can be useful if you are trying to install a package not published to the Pulsar Package Repository.
Pulsar can install from a GitHub repository or any valid git remote url. The -b
option can then be used to specify a particular tag or branch.
Git remotepulsar -p install <git_remote> [-b <branch_or_tag>]
GitHubpulsar -p install <github_username>/<github_project> [-b <branch_or_tag>]
For example to install the Generic-LSP package from GitHub you could use either:
pulsar -p install https://github.com/mauricioszabo/generic-lsp/
or
pulsar -p install mauricioszabo/generic-lsp
This will use the current HEAD commit of the default branch. If you want to install a specific version from a branch or tag then use the -b
option:
e.g. pulsar -p install https://github.com/mauricioszabo/generic-lsp/ -b v1.0.3
While it's pretty easy to move around Pulsar by clicking with the mouse or using the arrow keys, there are some keybindings that may help you keep your hands on the keyboard and navigate around a little faster.
You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
Pulsar also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the Command Palette, but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your keymap.cson
to create a key combination. You can open keymap.cson
file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu. For example, the command editor:move-to-beginning-of-screen-line
is available in the command palette, but it's not bound to any key combination. To create a key combination you need to add an entry in your keymap.cson
file. For editor:select-to-previous-word-boundary
, you can add the following to your keymap.cson
:
This will bind the command editor:select-to-previous-word-boundary
to LNX/WIN: Ctrl+Shift+E - MAC: Cmd+Shift+E. For more information on customizing your keybindings, see Customizing Keybindings.
Here's a list of Movement and Selection Commands that do not have a keyboard shortcut by default:
You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press LNX/WIN: Ctrl+R - MAC: Cmd+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to LNX/WIN: Ctrl+T - MAC: Cmd+T. You can also search for symbols across your project but it requires a tags
file.
You can generate a tags
file by using the ctags utility. Once it is installed, you can use it to generate a tags
file by running a command to generate it. See the ctags documentation for details.
You can customize how tags are generated by creating your own .ctags
file in your home directory, LNX/MAC: ~/.ctags
- WIN: %USERPROFILE%\.ctags
. An example can be found here.
The symbols navigation functionality is implemented in the symbols-view package.
Pulsar also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press LNX/WIN: Alt+Ctrl+F2 - MAC Cmd+F2, Pulsar will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Pulsar will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
The bookmarks functionality is implemented in the bookmarks package.
Text selections in Pulsar support a number of actions, such as scoping deletion, indentation and search actions, and marking text for actions such as quoting and bracketing.
Selections mirror many of the movement commands. They're actually exactly the same keybindings as the movement commands, but with a Shift key added in.
So far we've looked at a number of ways to move around and select regions of a file, so now let's actually change some of that text. Obviously you can type in order to insert characters, but there are also a number of ways to delete and manipulate text that could come in handy.
There are a handful of cool keybindings for basic text manipulation that might come in handy. These range from moving around lines of text and duplicating lines to changing the case.
Pulsar also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using LNX/WIN: Ctrl+Shift+Q - MAC: Alt+Cmd+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
One of the cool things that Pulsar can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.
Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any package or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the LNX/WIN: Ctrl - MAC: Cmd key pressed down to select multiple regions of your text simultaneously.
Return to a single cursor with Esc or by clicking anywhere in the file to position a cursor there as normal.
Pulsar comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
The whitespace commands are implemented in the whitespace package. The settings for the whitespace commands are managed on the page for the whitespace
package.
Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Pulsar, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Pulsar will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Pulsar ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Pulsar will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, “”
, ‘’
, «»
, ‹›
, and ``
when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Pulsar will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
The brackets functionality is implemented in the bracket-matcher package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.
Pulsar also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.
If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.
When you open a file, Pulsar will try to auto-detect the encoding. If Pulsar can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.
If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.
The encoding selector is implemented in the encoding-selector package.
Finding and replacing text in your file or project is quick and easy in Pulsar.
If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press LNX/WIN: Cmd+F - MAC: Ctrl+F, type in a search string and press LNX/WIN/MAC: Enter or LNX/WINF3 - MAC: Cmd+G or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurrences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Atom" with the string "Pulsar", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
Note
Note: Pulsar uses JavaScript regular expressions to perform regular expression searches.
When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, … $&. Refer to JavaScript's guide to regular expressions to learn more about regular expression syntax you can use in Pulsar.
You can also find and replace throughout your entire project if you invoke the panel with LNX/WIN: Ctrl+Shift+F - MAC: Cmd+Shift+F.
This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.
You can limit a search to a subset of the files in your project by entering a glob pattern into the "File/Directory pattern" text box. For example, the pattern src/*.js
would restrict the search to JavaScript files in the src
directory. The "globstar" pattern (**
) can be used to match arbitrarily many subdirectories. For example, docs/**/*.md
will match docs/a/foo.md
, docs/a/b/foo.md
, etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.
When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders /path1/folder1
and /path2/folder2
open, you could enter a pattern starting with folder1
to search only in the first folder.
Press Esc while focused on the Find and Replace panel to clear the pane from your workspace.
The Find and Replace functionality is implemented in the find-and-replace package and uses the scandal Node module to do the actual searching.
Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Pulsar, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Pulsar. You can also easily open up that file by selecting the LNX: Edit > Snippets - MAC: Pulsar > Snippets - WIN: File > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
Warning
Snippet keys, unlike CSS selectors, can only be repeated once per level. If there are duplicate keys at the same level, then only the last one will be read. See Configuring with CSON for more information.
You can also use CoffeeScript multi-line syntax using """
for larger templates:
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (${1:true}) {
+ $2
+ } else if (${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
💥 Just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Pulsar should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
Again, see Configuring with CSON for more information on CSON key structure and non-repeatability.
The snippets functionality is implemented in the snippets package.
For more examples, see the snippets in the language-html and language-javascript packages.
If you're still looking to save some typing time, Pulsar also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
The Autocomplete functionality is implemented in the autocomplete-plus package.
If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.
Tip
If you don't like using tabs, you don't have to. You can disable the tabs package and each pane will still support multiple pane items. You just won't have tabs to use to click between them.
"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
The "grammar" of a file is what language Pulsar has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in Snippets.
When you load a file, Pulsar does a little work to try to figure out what type of file it is. Largely this is accomplished by looking at its file extension (.md
is generally a Markdown file, etc.), though sometimes it has to inspect the content a bit to figure it out.
When you open a file and Pulsar can't determine a grammar for the file, it will default to "Plain Text", which is the simplest one. If it does default to "Plain Text", picks the wrong grammar for the file, or if for any reason you wish to change the selected grammar, you can pull up the Grammar Selector with Ctrl+Shift+L.
When the grammar of a file is changed, Atom will remember that for the current session.
The Grammar Selector functionality is implemented in the grammar-selector package.
Version control is an important aspect of any project and Pulsar comes with basic Git and GitHub integration built in.
In order to use version control in Pulsar, the project root needs to contain the Git repository.
The LNX/WIN: Alt+Ctrl+Z - MAC: Alt+Cmd+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use LNX/WIN: Ctrl+Z - MAC: Cmd+Z afterwards to restore the previous contents.
Pulsar ships with the fuzzy-finder package which provides LNX/WIN: Ctrl+T - MAC: Cmd+T to quickly open files in the project and LNX/WIN: Ctrl+B - MAC: Cmd+B to jump to any open editor. The package also provides LNX/WIN: Ctrl+Shift+B - MAC: Cmd+Shift+B which displays a list of all the untracked and modified files in the project. These will be the same files that you would see on the command line if you ran git status
.
An icon will appear to the right of each file letting you know whether it is untracked or modified.
Pulsar can be used as your Git commit editor and ships with the language-git package which adds syntax highlighting to edited commit, merge, and rebase messages.
You can configure Pulsar to be your Git commit editor with the following command:
$ git config --global core.editor "pulsar --wait"
+
The language-git package will help remind you to be brief by colorizing the first lines of commit messages when they're longer than 50 or 65 characters.
The status-bar package that ships with Pulsar includes several Git decorations that display on the right side of the status bar:
The currently checked out branch name is shown with the number of commits the branch is ahead of or behind its upstream branch. An icon is added if the file is untracked, modified, or ignored. The number of lines added and removed since the file was last committed will be displayed as well.
The included git-diff package colorizes the gutter next to lines that have been added, edited, or removed.
This package also adds Alt+G Down and Alt+GUp keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
The GitHub package brings Git and GitHub integration right inside Pulsar.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Pulsar and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the LNX/WIN: Cmd-Left - MAC: Ctrl-Left or LNX/WIN: Ctrl-Right - MAC: Cmd-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "👤➕" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Learn more about merge vs. rebase.
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
Though it is probably most common to use Pulsar to write software code, Pulsar can also be used to write prose quite effectively. Most often this is done in some sort of markup language such as Asciidoc or Markdown (in which this manual is written). Here we'll quickly cover a few of the tools Pulsar provides for helping you write prose.
In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Pulsar will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting LNX/WIN: Ctrl+Shift+; - MAC: Cmd+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Pulsar will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
The spell checking is implemented in the spell-check package.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Pulsar ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press LNX: Ctrl+Ins - WIN: Ctrl+C - MAC: Cmd+C or if you right-click in the preview pane and choose "Copy as HTML".
Markdown preview is implemented in the markdown-preview package.
There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing Snippets: Available
in the Command Palette.
Now that we are feeling comfortable with just about everything built into Pulsar, let's look at how to tweak it. Perhaps there is a keybinding that you use a lot but feels wrong or a color that isn't quite right for you. Pulsar is amazingly flexible, so let's go over some of the simpler flexes it can do.
All of Pulsar's config files (with the exception of your style sheet and your Init Script are written in CSON, short for CoffeeScript Object Notation. Just like its namesake JSON, JavaScript Object Notation, CSON is a text format for storing structured data in the form of simple objects made up of key-value pairs.
key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Pulsar's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory. You can open this file in an editor from the LNX: Edit > Stylesheet - MAC: Pulsar > Stylesheet - WIN: File > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Pulsar. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
Tip
If you are unfamiliar with Less, it is a basic CSS preprocessor that makes some things in CSS a bit easier. You can learn more about it at lesscss.org.
If you prefer to use CSS instead, you can do that in the same styles.less
file, since CSS is also valid in Less.
Pulsar keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Pulsar keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Pulsar's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Pulsar is started. It will always be loaded last, giving you the chance to override bindings that are defined by Pulsar's core keymaps or third-party packages. You can open this file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Pulsar in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the LNX/WIN: Ctrl+. - MAC: Cmd+. key combination. It will show you what keys Pulsar saw you press and what command Pulsar executed because of that combination.
Pulsar loads configuration settings from the config.cson
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the LNX: Edit > Config - MAC: Pulsar > Config - WIN: File > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of PulsarprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Pulsar will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Pulsar to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Pulsar to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Pulsar are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Pulsar Home directory will, by default, be called .pulsar
and will be located in the root of the home directory of the user.
An environment variable can be used to make Pulsar use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Pulsar Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Pulsar settings.
In addition to using the ATOM_HOME
environment variable, Pulsar can also be set to use "Portable Mode".
Portable Mode is most useful for taking Pulsar with you, with all your custom settings and packages, from machine to machine. This may take the form of keeping Pulsar on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Pulsar is in Portable Mode when there is a directory named .pulsar sibling to the directory in which the pulsar executable file lives. For example, the installed Pulsar directory can be placed into a Dropbox folder next to a .pulsar folder.
With such a setup, Pulsar will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Pulsar provides a command-line parameter option for setting Portable Mode.
$ pulsar --portable
+
Executing pulsar
with the --portable
option will take the .pulsar
directory you have in the default location (~/.pulsar
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
At this point you should be something of a Pulsar master user. You should be able to navigate and manipulate your text and files like a wizard. You should also be able to customize Pulsar backwards and forwards to make it look and act just how you want it to.
In the next section, we're going to kick it up a notch: we'll take a look at changing and adding new functionality to the core of Pulsar itself. We're going to start creating packages for Pulsar. If you can dream it, you can build it.
If you're still looking to save some typing time, Pulsar also ships with simple autocompletion functionality.
The autocomplete system lets you view and insert possible completions in the editor using Tab or Enter.
By default, the autocomplete system will look through the current open file for strings that match what you're starting to type.
If you want more options, in the Settings panel for the autocomplete-plus package you can toggle a setting to make autocomplete-plus look for text in all your open buffers rather than just the current file.
The Autocomplete functionality is implemented in the autocomplete-plus package.
Now that we are feeling comfortable with just about everything built into Pulsar, let's look at how to tweak it. Perhaps there is a keybinding that you use a lot but feels wrong or a color that isn't quite right for you. Pulsar is amazingly flexible, so let's go over some of the simpler flexes it can do.
All of Pulsar's config files (with the exception of your style sheet and your Init Script are written in CSON, short for CoffeeScript Object Notation. Just like its namesake JSON, JavaScript Object Notation, CSON is a text format for storing structured data in the form of simple objects made up of key-value pairs.
key:
+ key: value
+ key: value
+ key: [value, value]
+
Objects are the backbone of any CSON file, and are delineated by indentation (as in the above example). A key's value can either be a String, a Number, an Object, a Boolean, null
, or an Array of any of these data types.
Warning
Just like the more common JSON, CSON's keys can only be repeated once per object. If there are duplicate keys, then the last usage of that key overwrites all others, as if they weren't there. The same holds true for Pulsar's config files.
Don't do this:
# Only the second snippet will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+'.source.js':
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
Use this instead:
# Both snippets will be loaded
+'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+ 'console.error':
+ 'prefix': 'error'
+ 'body': 'console.error(${1:"crash"});$2'
+
If you want to apply quick-and-dirty personal styling changes without creating an entire theme that you intend to publish, you can add styles to the styles.less
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory. You can open this file in an editor from the LNX: Edit > Stylesheet - MAC: Pulsar > Stylesheet - WIN: File > Stylesheet menu.
For example, to change the colors of the Status Bar, you could add the following rule to your styles.less
file:
.status-bar {
+ color: white;
+ background-color: black;
+}
+
The easiest way to see what classes are available to style is to inspect the DOM manually via the Developer Tools. We'll go over the Developer Tools in great detail in the next chapter, but for now let's take a simple look. You can open the Developer Tools by pressing LNX/WIN: Ctrl+Shift+I - MAC: Alt+Cmd+I, which will bring up the Chromium Developer Tools panel.
With the Developer Tools, you can inspect all the elements in Pulsar. If you want to update the style of something, you can figure out what classes it has and add a Less rule to your stylesheet to modify it.
Tip
If you are unfamiliar with Less, it is a basic CSS preprocessor that makes some things in CSS a bit easier. You can learn more about it at lesscss.org.
If you prefer to use CSS instead, you can do that in the same styles.less
file, since CSS is also valid in Less.
Pulsar keymaps work similarly to stylesheets. Just as stylesheets use selectors to apply styles to elements, Pulsar keymaps use selectors to associate key combinations with events in specific contexts. Here's a small example, excerpted from Pulsar's built-in keymap:
'atom-text-editor':
+ 'enter': 'editor:newline'
+
+'atom-text-editor[mini] input':
+ 'enter': 'core:confirm'
+
This keymap defines the meaning of Enter in two different contexts. In a normal editor, pressing Enter triggers the editor:newline
command, which causes the editor to insert a newline. But if the same keystroke occurs inside a select list's mini-editor, it instead triggers the core:confirm
command based on the binding in the more-specific selector.
By default, keymap.cson
is loaded when Pulsar is started. It will always be loaded last, giving you the chance to override bindings that are defined by Pulsar's core keymaps or third-party packages. You can open this file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu.
You can see all the keybindings that are currently configured in your installation of Pulsar in the Keybindings tab in the Settings View.
If you run into problems with keybindings, the Keybinding Resolver is a huge help. It can be opened with the LNX/WIN: Ctrl+. - MAC: Cmd+. key combination. It will show you what keys Pulsar saw you press and what command Pulsar executed because of that combination.
Pulsar loads configuration settings from the config.cson
file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory.
'*':
+ 'core':
+ 'excludeVcsIgnoredPaths': true
+ 'editor':
+ 'fontSize': 18
+
The configuration is grouped into global settings under the *
key and language-specific settings under scope named keys like .python.source
or .html.text
. Underneath that, you'll find configuration settings grouped by package name or one of the two core namespaces: core
or editor
.
You can open this file in an editor from the LNX: Edit > Config - MAC: Pulsar > Config - WIN: File > Config menu.
core
customFileTypes
: Associations of language scope to file extensions (see Customizing Language Recognition)disabledPackages
: An array of package names to disableexcludeVcsIgnoredPaths
: Don't search within files specified by .gitignore
ignoredNames
: File names to ignore across all of PulsarprojectHome
: The directory where projects are assumed to be locatedthemes
: An array of theme names to load, in cascading ordereditor
autoIndent
: Enable/disable basic auto-indent (defaults to true
)nonWordCharacters
: A string of non-word characters to define word boundariesfontSize
: The editor font sizefontFamily
: The editor font familyinvisibles
: A hash of characters Pulsar will use to render whitespace characters. Keys are whitespace character types, values are rendered characters (use value false
to turn off individual whitespace character types) tab
: Hard tab characterscr
: Carriage return (for Microsoft-style line endings)eol
: \n
charactersspace
: Leading and trailing space characterslineHeight
: Height of editor lines, as a multiplier of font sizepreferredLineLength
: Identifies the length of a line (defaults to 80
)showInvisibles
: Whether to render placeholders for invisible characters (defaults to false
)showIndentGuide
: Show/hide indent indicators within the editorshowLineNumbers
: Show/hide line numbers within the guttersoftWrap
: Enable/disable soft wrapping of text within the editorsoftWrapAtPreferredLineLength
: Enable/disable soft line wrapping at preferredLineLength
tabLength
: Number of spaces within a tab (defaults to 2
)fuzzyFinder
ignoredNames
: Files to ignore only in the fuzzy-finderwhitespace
ensureSingleTrailingNewline
: Whether to reduce multiple newlines to one at the end of filesremoveTrailingWhitespace
: Enable/disable stripping of whitespace at the end of lines (defaults to true
)wrap-guide
columns
: Array of hashes with a pattern
and column
key to match the path of the current editor to a column position.You can also set several configuration settings differently for different file types. For example, you may want Pulsar to soft wrap markdown files, have two-space tabs for ruby files, and four-space tabs for python files.
There are several settings now scoped to an editor's language. Here is the current list:
editor.autoIndent
+editor.autoIndentOnPaste
+editor.invisibles
+editor.nonWordCharacters
+editor.preferredLineLength
+editor.scrollPastEnd
+editor.showIndentGuide
+editor.showInvisibles
+editor.softWrap
+editor.softWrapAtPreferredLineLength
+editor.softWrapHangingIndent
+editor.tabLength
+
You can edit these config settings in the Settings View on a per-language basis. Click on "Packages" tab in the navigation bar on the left, search for the language of your choice, select it, and edit away!
You can also edit the config.cson
directly. To open your configuration file via the Command Palette, press LNX/WIN: Ctrl+Shift+P - MAC: Cmd+Shift+P type open config
, and press Enter.
Global settings are under the *
key, and each language can have its own top-level key. This key is the language's scope. Language-specific settings take precedence over anything set in the global section for that language only.
'*': # all languages unless overridden
+ 'editor':
+ 'softWrap': false
+ 'tabLength': 8
+
+'.source.gfm': # markdown overrides
+ 'editor':
+ 'softWrap': true
+
+'.source.ruby': # ruby overrides
+ 'editor':
+ 'tabLength': 2
+
+'.source.python': # python overrides
+ 'editor':
+ 'tabLength': 4
+
In order to write these overrides effectively, you'll need to know the scope name for the language. We've already done this for finding a scope for writing a snippet in Snippet Format, but we can quickly cover it again.
The scope name is shown in the settings view for each language. Click on "Packages" in the navigation on the left, search for the language of your choice, select it, and you should see the scope name under the language name heading:
Another way to find the scope for a specific language is to open a file of its kind and:
These scopes can be especially useful to style the editor, since they can also be used as class names in your stylesheet.
If you want Pulsar to always recognize certain file types as a specific grammar, you'll need to manually edit your config.cson
file. You can open it using the Application: Open Your Config command from the Command Palette. For example, if you wanted to add the foo
extension to the CoffeeScript language, you could add this to your configuration file under the *.core
section:
'*':
+ core:
+ customFileTypes:
+ 'source.coffee': [
+ 'foo'
+ ]
+
In the example above, source.coffee
is the language's scope name (see Finding a Language's Scope Name for more information) and foo
is the file extension to match without the period. Adding a period to the beginning of either of these will not work.
The CSON configuration files for Pulsar are stored on disk on your machine. The location for this storage is customizable. The default is to use the home directory of the user executing the application. The Pulsar Home directory will, by default, be called .pulsar
and will be located in the root of the home directory of the user.
An environment variable can be used to make Pulsar use a different location. This can be useful for several reasons. One of these may be that multiple user accounts on a machine want to use the same Pulsar Home. The environment variable used to specify an alternate location is called ATOM_HOME
. If this environment variable exists, the location specified will be used to load and store Pulsar settings.
In addition to using the ATOM_HOME
environment variable, Pulsar can also be set to use "Portable Mode".
Portable Mode is most useful for taking Pulsar with you, with all your custom settings and packages, from machine to machine. This may take the form of keeping Pulsar on a USB drive or a cloud storage platform that syncs folders to different machines, like Dropbox. Pulsar is in Portable Mode when there is a directory named .pulsar sibling to the directory in which the pulsar executable file lives. For example, the installed Pulsar directory can be placed into a Dropbox folder next to a .pulsar folder.
With such a setup, Pulsar will use the same Home directory with the same settings for any machine with this directory syncronized/plugged in.
Pulsar provides a command-line parameter option for setting Portable Mode.
$ pulsar --portable
+
Executing pulsar
with the --portable
option will take the .pulsar
directory you have in the default location (~/.pulsar
) and copy the relevant contents for your configuration to a new home directory in the Portable Mode location. This enables easily moving from the default location to a portable operation without losing the customization you have already set up.
So far we've looked at a number of ways to move around and select regions of a file, so now let's actually change some of that text. Obviously you can type in order to insert characters, but there are also a number of ways to delete and manipulate text that could come in handy.
There are a handful of cool keybindings for basic text manipulation that might come in handy. These range from moving around lines of text and duplicating lines to changing the case.
Pulsar also has built in functionality to re-flow a paragraph to hard-wrap at a given maximum line length. You can format the current selection to have lines no longer than 80 (or whatever number editor.preferredLineLength
is set to) characters using LNX/WIN: Ctrl+Shift+Q - MAC: Alt+Cmd+Q. If nothing is selected, the current paragraph will be reflowed.
You can also delete or cut text out of your buffer with some shortcuts. Be ruthless.
One of the cool things that Pulsar can do out of the box is support multiple cursors. This can be incredibly helpful in manipulating long lists of text.
Using these commands you can place cursors in multiple places in your document and effectively execute the same commands in multiple places at once.
This can be incredibly helpful in doing many type of repetitive tasks such as renaming variables or changing the format of some text. You can use this with almost any package or command - for example, changing case and moving or duplicating lines.
You can also use the mouse to select text with the LNX/WIN: Ctrl - MAC: Cmd key pressed down to select multiple regions of your text simultaneously.
Return to a single cursor with Esc or by clicking anywhere in the file to position a cursor there as normal.
Pulsar comes with several commands to help you manage the whitespace in your document. One very useful pair of commands converts leading spaces into tabs and converts leading tabs into spaces. If you're working with a document that has mixed whitespace, these commands are great for helping to normalize the file. There are no keybindings for the whitespace commands, so you will have to search your command palette for "Convert Spaces to Tabs" (or vice versa) to run one of these commands.
The whitespace commands are implemented in the whitespace package. The settings for the whitespace commands are managed on the page for the whitespace
package.
Note
The "Remove Trailing Whitespace" option is on by default. This means that every time you save any file opened in Pulsar, it will strip all trailing whitespace from the file. If you want to disable this, go to the whitespace
package in your settings panel and uncheck that option.
Pulsar will also by default ensure that your file has a trailing newline. You can also disable this option on that screen.
Pulsar ships with intelligent and easy to use bracket handling.
It will by default highlight []
, ()
, and {}
style brackets when your cursor is over them. It will also highlight matching XML and HTML tags.
Pulsar will also automatically autocomplete []
, ()
, and {}
, ""
, ''
, “”
, ‘’
, «»
, ‹›
, and ``
when you type the leading one. If you have a selection and you type any of these opening brackets or quotes, Pulsar will enclose the selection with the opening and closing brackets or quotes.
There are a few other interesting bracket related commands that you can use.
The brackets functionality is implemented in the bracket-matcher package. Like all of these packages, to change defaults related to bracket handling, or to disable it entirely, you can navigate to this package in the Settings view.
Pulsar also ships with some basic file encoding support should you find yourself working with non-UTF-8 encoded files, or should you wish to create one.
If you pull up the file encoding dialog, you can choose an alternate file encoding to save your file in.
When you open a file, Pulsar will try to auto-detect the encoding. If Pulsar can't identify the encoding, the encoding will default to UTF-8, which is also the default encoding for new files.
If you pull up the encoding menu and change the active encoding to something else, the file will be written out in that encoding the next time you save the file.
The encoding selector is implemented in the encoding-selector package.
Finding and replacing text in your file or project is quick and easy in Pulsar.
If you launch either of those commands, you'll be greeted with the Find and Replace panel at the bottom of your screen.
To search within your current file you can press LNX/WIN: Cmd+F - MAC: Ctrl+F, type in a search string and press LNX/WIN/MAC: Enter or LNX/WINF3 - MAC: Cmd+G or the "Find Next" button) multiple times to cycle through all the matches in that file. Alt+Enter will find all occurrences of the search string. The Find and Replace panel also contains buttons for toggling case sensitivity, performing regular expression matching, scoping the search to selections, and performing whole word search.
If you type a string in the replacement text box, you can replace matches with a different string. For example, if you wanted to replace every instance of the string "Atom" with the string "Pulsar", you would enter those values in the two text boxes and press the "Replace All" button to perform the replacements.
Note
Note: Pulsar uses JavaScript regular expressions to perform regular expression searches.
When doing a regular expression search, the replacement syntax to refer back to search groups is $1, $2, … $&. Refer to JavaScript's guide to regular expressions to learn more about regular expression syntax you can use in Pulsar.
You can also find and replace throughout your entire project if you invoke the panel with LNX/WIN: Ctrl+Shift+F - MAC: Cmd+Shift+F.
This is a great way to find out where in your project a function is called, an anchor is linked to or a specific misspelling is located. Click on the matching line to jump to that location in that file.
You can limit a search to a subset of the files in your project by entering a glob pattern into the "File/Directory pattern" text box. For example, the pattern src/*.js
would restrict the search to JavaScript files in the src
directory. The "globstar" pattern (**
) can be used to match arbitrarily many subdirectories. For example, docs/**/*.md
will match docs/a/foo.md
, docs/a/b/foo.md
, etc. You can enter multiple glob patterns separated by commas, which is useful for searching in multiple file types or subdirectories.
When you have multiple project folders open, this feature can also be used to search in only one of those folders. For example, if you had the folders /path1/folder1
and /path2/folder2
open, you could enter a pattern starting with folder1
to search only in the first folder.
Press Esc while focused on the Find and Replace panel to clear the pane from your workspace.
The Find and Replace functionality is implemented in the find-and-replace package and uses the scandal Node module to do the actual searching.
If you want to see an overview of the structure of the code file you're working on, folding can be a helpful tool. Folding hides blocks of code such as functions or looping blocks in order to simplify what is on your screen.
The GitHub package brings Git and GitHub integration right inside Pulsar.
Most of the functionality lives within the Git and GitHub dock items.
There are different ways to access them, probably the most common way is through their keybindings:
Another way is from the menu: Packages -> GitHub -> Toggle Git Tab and Toggle GitHub Tab
Or you can also toggle the Git panel from the Status Bar by clicking on the changed files icon:
In case a project doesn't have a Git repository yet, you can create one from the Git panel.
To clone a repository, open the GitHub panel while you have no project folders open in Pulsar and click "Clone an existing GitHub repository". In the dialog, paste the URL of a repository and click "Clone". The new project will be added to the Tree View.
Alternately, run the GitHub: Clone
command to open the Clone dialog any time.
To open the branch tooltip, click the branch icon in the Status Bar. From there you can create or switch branches.
After making some changes, stage anything you want to be part of the next commit. Choose between staging...
Use the LNX/WIN: Cmd-Left - MAC: Ctrl-Left or LNX/WIN: Ctrl-Right - MAC: Cmd-Right arrow key to switch between file list and the diff view. Unstaging can be done in the same way.
If you no longer want to keep some changes, you can discard them. It's similar to staging, but accessible behind a context menu.
To double check all changes that are going into your next commit, click the "See All Staged Changes" button above the commit message box. It lets you see all of your staged changes in a single pane. This "commit preview" can also serve as an inspiration for writing the commit message.
Once you've staged your changes, enter a commit message. Feel free to describe the commit in more detail after leaving an empty line. Finalize by clicking the Commit button. If you need more space, click the expand icon at the bottom right. It will open a commit editor in the center.
To add multiple co-authors to a commit, click the "👤➕" icon in the bottom left corner of the commit message editor. Now you can search by name, email or GitHub username to give credit to a co-author.
In case you forgot to commit a change and would like to add it to your previous commit, right-click on the last commit, then choose "Amend" from the context menu.
If you want to edit the commit message of your last commit, or add/remove changes, click on the "Undo" button. It will roll back to the state just before you clicked on the commit button.
Once you've made some commits, click on a commit message in the recent commit list to see the full diff and commit message associated with each:
When you're ready to share your changes with your team members, click the Publish button in the Status Bar. It will push your local branch to the remote repository. After making more commits, you can Push them as well from the Status Bar.
From time to time it's a good idea to click on the Fetch button to see if any other team member pushed changes. If so, click on Pull to merge the changes into your local branch.
If you prefer to rebase when pulling, you can configure Git to make it the default behavior:
git config --global --bool pull.rebase true
+
Learn more about merge vs. rebase.
Sometimes there can be conflicts when trying to merge. Files that have merge conflicts will show up in the "Merge Conflicts" list. Click on a file to open the editor. There you can resolve the conflict by picking a version or make further edits. Once done, stage the file and commit.
When your changes are ready to be reviewed by your team members, open the "GitHub" panel Ctrl+8 and click on Open new pull request. It will open the browser where you can continue creating a pull request. If commits haven't been pushed or the branch isn't published yet, the GitHub package will do that automatically for you.
Once the pull request is created, it will appear under Current pull request at the top of the panel. Underneath is a list of Open pull requests. It lets you quickly find a pull request by avatar, title or PR number. It also lets you keep an eye on the CI status. Clicking on a pull request in the list opens a center pane with more details, the timeline and conversations.
You can open issues or pull requests from any repo on GitHub. To do so, run the GitHub: Open Issue Or Pull Request
command and paste the URL from an issue or pull request. Then press the Open Issue or Pull Request button and it will open a center pane. This lets you keep an issue or pull request as a reference, when working in another repo.
To test a pull request locally, open it in the workspace center by clicking on the pull request in the "open pull requests" list from the GitHub tab, then click on the Checkout button. It will automatically create a local branch and pull all the changes. If you would like to contribute to that pull request, start making changes, commit and push. Your contribution is now part of that pull request.
To view review comments on a Pull Request, open the Reviews Tab from the See Reviews button from the footer of a Pull Request Pane. Alternatively, if the pull request has already been checked out, Reviews Tab can also be open from the same button on GitHub Tab.
You can see all the review summaries and comments of a pull request in the Reviews Tab. The comment section has a progress bar to help you keep track of how close are you to finish addressing the Pull Request comments (i.e. marking all comment threads on a Pull Request as "resolved"). Comment threads are greyed out after they have been resolved.
After the pull request branch has been checked out, you can click Jump To File to open the commented on file and make changes as per the review comment right in the editor. If you would like to get the full context of the review comment, click Open Diff to open the diff view with line highlighting.
Conversely, in-editor comments are indicated by the comment icon in the gutter. Clicking the icon, either from within the editor or the diff view, will take you back to the Reviews Tab.
To respond to a Pull Request review comment, type your message and click Comment; a single line comment will be created in the same thread as the comment you responded to. After addressing a Pull Request review comment, click Resolve conversation to mark the whole thread as "resolved". The progress bar in the "Comments" section will update accordingly.
The "grammar" of a file is what language Pulsar has associated with that file. Types of grammars would include "Java" or "GitHub-Flavored Markdown". We looked at this a bit when we created some snippets in Snippets.
When you load a file, Pulsar does a little work to try to figure out what type of file it is. Largely this is accomplished by looking at its file extension (.md
is generally a Markdown file, etc.), though sometimes it has to inspect the content a bit to figure it out.
When you open a file and Pulsar can't determine a grammar for the file, it will default to "Plain Text", which is the simplest one. If it does default to "Plain Text", picks the wrong grammar for the file, or if for any reason you wish to change the selected grammar, you can pull up the Grammar Selector with Ctrl+Shift+L.
When the grammar of a file is changed, Atom will remember that for the current session.
The Grammar Selector functionality is implemented in the grammar-selector package.
While it's pretty easy to move around Pulsar by clicking with the mouse or using the arrow keys, there are some keybindings that may help you keep your hands on the keyboard and navigate around a little faster.
You can also move directly to a specific line (and column) number with Ctrl+G. This will bring up a dialog that asks which line you would like to jump to. You can also use the row:column
syntax to jump to a character in that line as well.
Pulsar also has a few movement and selection commands that don't have keybindings by default. You can access these commands from the Command Palette, but if you find yourself using commands that don't have a keybinding often, have no fear! You can easily add an entry to your keymap.cson
to create a key combination. You can open keymap.cson
file in an editor from the LNX: Edit > Keymap - MAC: Pulsar > Keymap - WIN: File > Keymap menu. For example, the command editor:move-to-beginning-of-screen-line
is available in the command palette, but it's not bound to any key combination. To create a key combination you need to add an entry in your keymap.cson
file. For editor:select-to-previous-word-boundary
, you can add the following to your keymap.cson
:
This will bind the command editor:select-to-previous-word-boundary
to LNX/WIN: Ctrl+Shift+E - MAC: Cmd+Shift+E. For more information on customizing your keybindings, see Customizing Keybindings.
Here's a list of Movement and Selection Commands that do not have a keyboard shortcut by default:
You can also jump around a little more informatively with the Symbols View. To jump to a symbol such as a method definition, press LNX/WIN: Ctrl+R - MAC: Cmd+R. This opens a list of all symbols in the current file, which you can fuzzy filter similarly to LNX/WIN: Ctrl+T - MAC: Cmd+T. You can also search for symbols across your project but it requires a tags
file.
You can generate a tags
file by using the ctags utility. Once it is installed, you can use it to generate a tags
file by running a command to generate it. See the ctags documentation for details.
You can customize how tags are generated by creating your own .ctags
file in your home directory, LNX/MAC: ~/.ctags
- WIN: %USERPROFILE%\.ctags
. An example can be found here.
The symbols navigation functionality is implemented in the symbols-view package.
Pulsar also has a great way to bookmark specific lines in your project so you can jump back to them quickly.
If you press LNX/WIN: Alt+Ctrl+F2 - MAC Cmd+F2, Pulsar will toggle a "bookmark" on the current line. You can set these throughout your project and use them to quickly find and jump to important lines of your project. A small bookmark symbol is added to the line gutter, like on line 22 of the image below.
If you hit F2, Pulsar will jump to the next bookmark in the file you currently have focused. If you use Shift+F2 it will cycle backwards through them instead.
You can also see a list of all your project's current bookmarks and quickly filter them and jump to any of them by hitting Ctrl+F2.
The bookmarks functionality is implemented in the bookmarks package.
Tip
If you don't like using tabs, you don't have to. You can disable the tabs package and each pane will still support multiple pane items. You just won't have tabs to use to click between them.
"Pending Pane Items" were formerly referred to as "Preview Tabs"
When you open a new file by single-clicking in the Tree View, it will open in a new tab with an italic title. This indicates that the file is "pending". When a file is pending, it will be replaced by the next pending file that is opened. This allows you to click through a bunch of files to find something without having to go back and close them all.
You can confirm a pending file by doing any of the following:
You can also open a file already confirmed by double-clicking it in the tree view instead of single-clicking it.
If you would prefer to not have files open in pending form, you can disable this behavior by unchecking "Allow Pending Pane Items" in the Core Settings section of the Settings View. With pending pane items disabled, single-clicking a file in the Tree View will select the file but not open it. You will have to double-click the file to open it.
First we'll start with the Pulsar package system. As we mentioned previously, Pulsar itself is a very basic core of functionality that ships with a number of useful packages that add new features like the Tree View and the Settings View.
In fact, there are more than 80 packages that comprise all of the functionality that is available in Pulsar by default. For example, the Welcome screen that you see when you first start Pulsar, the spell checker, the themes and the Fuzzy Finder are all packages that are separately maintained and all use the same APIs that you have access to, as we'll see in great detail in Hacking the Core.
This means that packages can be incredibly powerful and can change everything from the very look and feel of the entire interface to the basic operation of even core functionality.
In order to install a new package, you can use the Install tab in the now familiar Settings View. Open up the Settings View using LNX/WIN: Ctrl+, - MAC: Cmd+, click on the "Install" tab and type your search query into the box under Install Packages.
The packages listed here have been published to https://web.pulsar-edit.dev which is the official registry for Pulsar packages. Searching on the Settings View will go to the Pulsar package registry and pull in anything that matches your search terms.
All of the packages will come up with an "Install" button. Clicking that will download the package and install it. Your editor will now have the functionality that the package provides.
Once a package is installed in Pulsar, it will show up in the Settings View under the "Packages" tab, along with all the pre-installed packages that come with Pulsar. To filter the list in order to find one, you can type into search box directly under the "Installed Packages" heading.
Clicking on the "Settings" button for a package will give you the settings screen for that package specifically. Here you have the option of changing some of the default variables for the package, seeing what all the command keybindings are, disabling the package temporarily, looking at the source code, seeing the current version of the package, reporting issues and uninstalling the package.
If a new version of any of your packages is released, Pulsar will automatically detect it and you can upgrade the package from either this screen or from the "Updates" tab. This helps you easily keep all your installed packages up to date.
You can also find and install new themes for Pulsar from the Settings View. These can be either UI themes or syntax themes and you can search for them from the "Install" tab, just like searching for new packages. Make sure to press the "Themes" toggle next to the search box.
Clicking on the theme title will take you to a profile page for the theme on pulsar-edit.dev, which often has a screenshot of the theme. This way you can see what it looks like before installing it.
Clicking on "Install" will install the theme and make it available in the Theme dropdowns as we saw in Changing the Theme.
You can also install packages or themes from the command line using ppm (Pulsar Package Manager). This is used by running pulsar -p <commmand>
or pulsar --package <command>
.
Tip
Check that you have ppm available by running the following command in your terminal:
$ pulsar -p help install
+
You should see a message print out with details about the pulsar -p install
command.
If you do not, see the Installing Pulsar section for instructions on how to install the pulsar
command for your system.
You can install packages by using the pulsar -p install
command:
pulsar -p install <package_name>
to install the latest version.pulsar -p install <package_name>@<package_version>
to install a specific version.For example pulsar -p install minimap@4.40.0
installs the 4.40.0
release of the minimap package.
You can also use ppm to find new packages to install. If you run pulsar -p search
, you can search the package registry for a search term.
$ pulsar -p search linter
+> Search Results For 'linter' (30)
+> ├── linter A Base Linter with Cow Powers (9863242 downloads, 4757 stars)
+> ├── linter-ui-default Default UI for the Linter package (7755748 downloads, 1201 stars)
+> ├── linter-eslint Lint JavaScript on the fly, using ESLint (v7 or older) (2418043 downloads, 1660 stars)
+> ├── linter-jshint Linter plugin for JavaScript, using jshint (1202044 downloads, 1271 stars)
+> ├── linter-gcc Lint C and C++ source files using gcc / g++ (863989 downloads, 194 stars)
+> ...
+> ├── linter-shellcheck Lint Bash on the fly, using shellcheck (136938 downloads, 280 stars)
+> └── linter-rust Lint Rust-files, using rustc and/or cargo (132550 downloads, 91 stars)
+
You can use pulsar -p view
to see more information about a specific package.
$ pulsar -p view linter
+> linter
+> ├── 3.4.0
+> ├── https://github.com/steelbrain/linter
+> ├── A Base Linter with Cow Powers
+> ├── 9863242 downloads
+> └── 4757 stars
+>
+> Run `pulsar -p install linter` to install this package.
+
By default ppm will be using the Pulsar Package Repository. However you can also use it to install from other locations which can be useful if you are trying to install a package not published to the Pulsar Package Repository.
Pulsar can install from a GitHub repository or any valid git remote url. The -b
option can then be used to specify a particular tag or branch.
Git remotepulsar -p install <git_remote> [-b <branch_or_tag>]
GitHubpulsar -p install <github_username>/<github_project> [-b <branch_or_tag>]
For example to install the Generic-LSP package from GitHub you could use either:
pulsar -p install https://github.com/mauricioszabo/generic-lsp/
or
pulsar -p install mauricioszabo/generic-lsp
This will use the current HEAD commit of the default branch. If you want to install a specific version from a branch or tag then use the -b
option:
e.g. pulsar -p install https://github.com/mauricioszabo/generic-lsp/ -b v1.0.3
Text selections in Pulsar support a number of actions, such as scoping deletion, indentation and search actions, and marking text for actions such as quoting and bracketing.
Selections mirror many of the movement commands. They're actually exactly the same keybindings as the movement commands, but with a Shift key added in.
Snippets are an incredibly powerful way to quickly generate commonly needed code syntax from a shortcut.
The idea is that you can type something like habtm
and then press the Tab key and it will expand into has_and_belongs_to_many
.
Many Core and Community packages come bundled with their own snippets that are specific to it. For example, the language-html
package that provides support for HTML syntax highlighting and grammar comes with dozens of snippets to create many of the various HTML tags you might want to use. If you create a new HTML file in Pulsar, you can type html
and then press Tab and it will expand to:
<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ </head>
+ <body></body>
+</html>
+
It will also position the cursor in the lang
attribute value so you can edit it if necessary. Many snippets have multiple focus points that you can move through with the Tab key as well - for instance, in the case of this HTML snippet, after the cursor is placed in the lang
attribute value, you can continue pressing Tab and the cursor will move to the dir
attribute value, then to the middle of the title
tag, then finally to the middle of the body
tag.
To see all the available snippets for the file type that you currently have open, choose "Snippets: Available" in the Command Palette.
You can also use fuzzy search to filter this list down by typing in the selection box. Selecting one of them will execute the snippet where your cursor is (or multiple cursors are).
So that's pretty cool, but what if there is something the language package didn't include or something that is custom to the code you write? Luckily it's incredibly easy to add your own snippets.
There is a text file in your LNX/MAC: ~/.pulsar
- WIN: %USERPROFILE%\.pulsar
directory called snippets.cson
that contains all your custom snippets that are loaded when you launch Pulsar. You can also easily open up that file by selecting the LNX: Edit > Snippets - MAC: Pulsar > Snippets - WIN: File > Snippets menu.
So let's look at how to write a snippet. The basic snippet format looks like this:
'.source.js':
+ 'console.log':
+ 'prefix': 'log'
+ 'body': 'console.log(${1:"crash"});$2'
+
The leftmost keys are the selectors where these snippets should be active. The easiest way to determine what this should be is to go to the language package of the language you want to add a snippet for and look for the "Scope" string.
For example, if we wanted to add a snippet that would work for Java files, we would look up the language-java
package in our Settings view and we can see the Scope is source.java
. Then the top level snippet key would be that prepended by a period (like a CSS class selector would do).
The next level of keys are the snippet names. These are used for describing the snippet in a more readable way in the snippet menu. You can name them whatever you want.
Under each snippet name is a prefix
that should trigger the snippet and a body
to insert when the snippet is triggered.
Each $
followed by a number is a tab stop. Tab stops are cycled through by pressing Tab once a snippet has been triggered.
Tab stops with the same number will create multiple cursors.
The above example adds a log
snippet to JavaScript files that would expand to:
console.log("crash");
+
The string "crash"
would be initially selected and pressing tab again would place the cursor after the ;
Warning
Snippet keys, unlike CSS selectors, can only be repeated once per level. If there are duplicate keys at the same level, then only the last one will be read. See Configuring with CSON for more information.
You can also use CoffeeScript multi-line syntax using """
for larger templates:
'.source.js':
+ 'if, else if, else':
+ 'prefix': 'ieie'
+ 'body': """
+ if (${1:true}) {
+ $2
+ } else if (${3:false}) {
+ $4
+ } else {
+ $5
+ }
+ """
+
As you might expect, there is a snippet to create snippets. If you open up a snippets file and type snip
and then press Tab, you will get the following text inserted:
'.source.js':
+ 'Snippet Name':
+ 'prefix': 'hello'
+ 'body': 'Hello World!'
+
💥 Just fill that bad boy out and you have yourself a snippet. As soon as you save the file, Pulsar should reload the snippets and you will immediately be able to try it out.
You can see below the format for including multiple snippets for the same scope in your snippets.cson
file. Just include the snippet name, prefix, and body keys for additional snippets inside the scope key:
'.source.gfm':
+ 'Hello World':
+ 'prefix': 'hewo'
+ 'body': 'Hello World!'
+
+ 'Github Hello':
+ 'prefix': 'gihe'
+ 'body': 'Octocat says Hi!'
+
+ 'Octocat Image Link':
+ 'prefix': 'octopic'
+ 'body': '![GitHub Octocat](https://assets-cdn.github.com/images/modules/logos_page/Octocat.png)'
+
Again, see Configuring with CSON for more information on CSON key structure and non-repeatability.
The snippets functionality is implemented in the snippets package.
For more examples, see the snippets in the language-html and language-javascript packages.
At this point you should be something of a Pulsar master user. You should be able to navigate and manipulate your text and files like a wizard. You should also be able to customize Pulsar backwards and forwards to make it look and act just how you want it to.
In the next section, we're going to kick it up a notch: we'll take a look at changing and adding new functionality to the core of Pulsar itself. We're going to start creating packages for Pulsar. If you can dream it, you can build it.
Version control is an important aspect of any project and Pulsar comes with basic Git and GitHub integration built in.
In order to use version control in Pulsar, the project root needs to contain the Git repository.
The LNX/WIN: Alt+Ctrl+Z - MAC: Alt+Cmd+Z keybinding checks out the HEAD
revision of the file in the editor.
This is a quick way to discard any saved and staged changes you've made and restore the file to the version in the HEAD
commit. This is essentially the same as running git checkout HEAD -- <path>
and git reset HEAD -- <path>
from the command line for that path.
This command goes onto the undo stack so you can use LNX/WIN: Ctrl+Z - MAC: Cmd+Z afterwards to restore the previous contents.
Pulsar ships with the fuzzy-finder package which provides LNX/WIN: Ctrl+T - MAC: Cmd+T to quickly open files in the project and LNX/WIN: Ctrl+B - MAC: Cmd+B to jump to any open editor. The package also provides LNX/WIN: Ctrl+Shift+B - MAC: Cmd+Shift+B which displays a list of all the untracked and modified files in the project. These will be the same files that you would see on the command line if you ran git status
.
An icon will appear to the right of each file letting you know whether it is untracked or modified.
Pulsar can be used as your Git commit editor and ships with the language-git package which adds syntax highlighting to edited commit, merge, and rebase messages.
You can configure Pulsar to be your Git commit editor with the following command:
$ git config --global core.editor "pulsar --wait"
+
The language-git package will help remind you to be brief by colorizing the first lines of commit messages when they're longer than 50 or 65 characters.
The status-bar package that ships with Pulsar includes several Git decorations that display on the right side of the status bar:
The currently checked out branch name is shown with the number of commits the branch is ahead of or behind its upstream branch. An icon is added if the file is untracked, modified, or ignored. The number of lines added and removed since the file was last committed will be displayed as well.
The included git-diff package colorizes the gutter next to lines that have been added, edited, or removed.
This package also adds Alt+G Down and Alt+GUp keybindings that allow you to move the cursor to the next or previous diff in the current editor.
If the project you're working on is on GitHub, there are also some very useful integrations you can use. Most of the commands will take the current file you're viewing and open a view of that file on GitHub - for instance, the blame or commit history of that file.
The branch comparison shows you the commits that are on the branch you're currently working on locally that are not on the mainline branch.
Though it is probably most common to use Pulsar to write software code, Pulsar can also be used to write prose quite effectively. Most often this is done in some sort of markup language such as Asciidoc or Markdown (in which this manual is written). Here we'll quickly cover a few of the tools Pulsar provides for helping you write prose.
In these docs, we'll concentrate on writing in Markdown; however, other prose markup languages like Asciidoc have packages that provide similar functionality.
If you're working in text (which includes plain text files, GitHub markdown, and Git commit messages by default), Pulsar will automatically try to check your spelling.
Any misspelled words will be highlighted (by default with a dashed red line beneath the word), and you can pull up a menu of possible corrections by hitting LNX/WIN: Ctrl+Shift+; - MAC: Cmd+Shift+; (or by choosing "Correct Spelling" from the right-click context menu or from the Command Palette).
To add more types of files to the list of what Pulsar will try to spell check, go to the Spell Check package settings in your Settings view and add any grammars you want to spell check.
The default grammars to spell check are text.plain
, source.gfm
, text.git-commit
, source.asciidoc
, source.rst
, and text.restructuredtext
but you can add other grammars if you wish to check those types of files too.
The spell checking is implemented in the spell-check package.
When writing prose in a markup language, it's often very useful to get an idea of what the content will look like when it's rendered. Pulsar ships with a package for previewing Markdown by default.
As you edit the text, the preview will also update automatically. This makes it fairly easy to check your syntax as you type.
You can also copy the rendered HTML from the preview pane into your system clipboard when the preview is focused and you press LNX: Ctrl+Ins - WIN: Ctrl+C - MAC: Cmd+C or if you right-click in the preview pane and choose "Copy as HTML".
Markdown preview is implemented in the markdown-preview package.
There are also a number of great snippets available for writing Markdown quickly.
If you type img
and hit tab
you get a Markdown-formatted image embed code like ![]()
. If you type table
and hit tab
you get a nice example table to fill out.
| Header One | Header Two |
+| :--------- | :--------- |
+| Item One | Item Two |
+
Although there are only a handful of Markdown snippets (b
for bold, i
for italic, code
for a code block, etc), they save you from having to look up the more obscure syntaxes. Again, you can easily see a list of all available snippets for the type of file you're currently in by choosing Snippets: Available
in the Command Palette.
Welcome to the atom-languageclient
wiki!
This wiki has been nearly directly taken from the upstream Atom Wiki. That may mean that parts of it are out of date, but otherwise hopefully is a helpful resource relating to our Atom-LanguageClient Repo!
atom-languageclient
Note
Please note that its possible this list is outdated, as its original version was published by @'Damien Guard' on Apr 21, 2018.
Here are the known Pulsar packages that are using atom-languageclient
today:
Note
Please note that its possible this is outdated, as its original version was published by @'David Wilson' on Jan 22, 2018.
master
.## N.N.N
section header.npm run test
and verify that no tests failed.npm run flow
and verify that there are no errors.npm version [version type]
where the version type is major
, minor
, or path
depending on the semantic versioning impact of the changes.git log -1
and make sure a commit was made for the new version (double-check version in package.json if you like).npm publish
to publish the package.git push --tags
to push the new version tag to GitHub.Note
Please note that its possible this is outdated, as its original version was published by @'percova' on Nov 12, 2019.
Grammar | Selector | Provider | Status |
---|---|---|---|
All | * | SymbolProvider | Default Provider |
All | * | FuzzyProvider | Deprecated |
Grammar | Selector | Provider | API Status |
---|---|---|---|
Null Grammar | .text.plain.null-grammar | ||
CoffeeScript (Literate) | .source.litcoffee | ||
CoffeeScript | .source.coffee | ||
JSON | .source.json | ||
Shell Session | .text.shell-session | ||
Shell Script | .source.shell | ||
Hyperlink | .text.hyperlink | ||
TODO | .text.todo | ||
C | .source.c | autocomplete-clang | |
C++ | .source.cpp | autocomplete-clang | 2.0.0 |
Clojure | .source.clojure | proto-repl | |
CSS | .source.css | autocomplete-css | 2.0.0 |
GitHub Markdown | .source.gfm | autocomplete-bibtex | 1.1.0 |
Git Config | .source.git-config | ||
Git Commit Message | .text.git-commit | ||
Git Rebase Message | .text.git-rebase | ||
HTML (Go) | .text.html.gohtml | ||
Go | .source.go | go-plus, autocomplete-go | 2.0.0 |
Go Template | .source.gotemplate | ||
HTML | .text.html.basic | autocomplete-html | 2.0.0 |
JavaScript | .source.js | atom-ternjs | 2.0.0 |
Java Properties | .source.java-properties | ||
Regular Expressions (JavaScript) | .source.js.regexp | ||
JavaServer Pages | .text.html.jsp | autocomplete-jsp | 2.0.0 |
Java | .source.java | autocomplete-java-minus | 2.0.0 |
JUnit Test Report | .text.junit-test-report | ||
Makefile | .source.makefile | ||
LESS | .source.css.less | ||
SQL (Mustache) | .source.sql.mustache | ||
HTML (Mustache) | .text.html.mustache | ||
Objective-C++ | .source.objcpp | autocomplete-clang | |
Strings File | .source.strings | ||
Objective-C | .source.objc | autocomplete-clang | |
Property List (XML) | .text.xml.plist | ||
Property List (Old-Style) | .source.plist | ||
Perl | .source.perl | ||
PHP | .text.html.php | ||
PHP | .source.php | php-integrator-autocomplete-plus, atom-autocomplete-php, autocomplete-php | 2.0.0 |
Python Console | .text.python.console | ||
Python Traceback | .text.python.traceback | ||
Regular Expressions (Python) | .source.regexp.python | ||
Python | .source.python | autocomplete-python, autocomplete-python-jedi | |
Ruby on Rails (RJS) | .source.ruby.rails.rjs | ||
Ruby | .source.ruby | ||
HTML (Ruby - ERB) | .text.html.erb | ||
HTML (Rails) | .text.html.ruby | ||
SQL (Rails) | .source.sql.ruby | ||
JavaScript (Rails) | .source.js.rails .source.js.jquery | ||
Ruby on Rails | .source.ruby.rails | ||
Sass | .source.sass | ||
Plain Text | .text.plain | ||
SCSS | .source.css.scss | ||
SQL | .source.sql | autocomplete-sql | 2.0.0 |
TOML | .source.toml | ||
XSL | .text.xml.xsl | ||
XML | .text.xml | autocomplete-xml | 2.0.0 |
YAML | .source.yaml |
Selector | Provider | Status |
---|---|---|
* | autocomplete-emojis | 1.0.0 |
* | autocomplete-snippets | 2.0.0 |
* | autocomplete-paths | 1.0.0 |
* | atom-path-intellisense | 1.2.1 |
* | atom-ctags | 2.0.0 |
.source.js, .source.jsx | ide-flow | 1.1.0 |
.source.js, .source.jsx, .source.coffee | autocomplete-underdash | 2.0.0 |
.source.css , .source.css.less , .source.sass , .source.css.scss , .source.stylus | project-palette-finder | 1.1.0 |
* | you-complete-me | 2.0.0 |
English word autocompetion with the hint of explanation. | autocomplete-en-en | 2.0.0 |
--
If you'd like to contribute and are interested in learning how to write an autocomplete-plus
Provider
, start here:
If you are using one of these providers, please uninstall the package as it is no longer maintained.
autocomplete
)Welcome to the autocomplete-plus
wiki!
This wiki has been nearly directly taken from the taken from the upstream Atom Wiki. That may mean that parts of it are out of date, but otherwise hopefully is a helpful resource relating to our Autocomplete-Plus Repo.
Note
Please note that its possible this is outdated, as its original version was published by @'Nathan Sobo' on Jan 3, 2018.
The Provider API allows you to make autocomplete+ awesome. The Pulsar community will ultimately judge the quality of Pulsar's autocomplete experience by the breadth and depth of the provider ecosystem. We're so excited that you're here reading about how to make Pulsar awesome.
The following examples are in CoffeeScript. If you would like to add JavaScript examples, please feel free to edit this page!
provider =
+ # This will work on JavaScript and CoffeeScript files, but not in js comments.
+ selector: '.source.js, .source.coffee'
+ disableForSelector: '.source.js .comment'
+
+ # This will take priority over the default provider, which has an inclusionPriority of 0.
+ # `excludeLowerPriority` will suppress any providers with a lower priority
+ # i.e. The default provider will be suppressed
+ inclusionPriority: 1
+ excludeLowerPriority: true
+
+ # This will be suggested before the default provider, which has a suggestionPriority of 1.
+ suggestionPriority: 2
+
+ # Let autocomplete+ filter and sort the suggestions you provide.
+ filterSuggestions: true
+
+ # Required: Return a promise, an array of suggestions, or null.
+ getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix, activatedManually}) ->
+ new Promise (resolve) ->
+ resolve([text: 'something'])
+
+ # (optional): (*experimental*) called when user the user selects a suggestion for the purpose of loading additional information about the suggestion.
+ getSuggestionDetailsOnSelect: (suggestion) ->
+ new Promise (resolve) ->
+ resolve(newSuggestion)
+
+ # (optional): called _after_ the suggestion `replacementPrefix` is replaced
+ # by the suggestion `text` in the buffer
+ onDidInsertSuggestion: ({editor, triggerPosition, suggestion}) ->
+
+ # (optional): called when your provider needs to be cleaned up. Unsubscribe
+ # from things, kill any processes, etc.
+ dispose: ->
+
The properties of a provider:
selector
(required): Defines the scope selector(s) (can be comma-separated) for which your provider should receive suggestion requestsgetSuggestions
(required): Is called when a suggestion request has been dispatched by autocomplete+
to your provider. Return an array of suggestions (if any) in the order you would like them displayed to the user. Returning a Promise of an array of suggestions is also supported.getSuggestionDetailsOnSelect
(optional): (experimental) Is called when a suggestion is selected by the user for the purpose of loading more information about the suggestion. Return a Promise of the new suggestion to replace it with or return null
if no change is needed.disableForSelector
(optional): Defines the scope selector(s) (can be comma-separated) for which your provider should not be usedinclusionPriority
(optional): A number to indicate its priority to be included in a suggestions request. The default provider has an inclusion priority of 0
. Higher priority providers can suppress lower priority providers with excludeLowerPriority
.excludeLowerPriority
(optional): Will not use lower priority providers when this provider is used.suggestionPriority
(optional): A number to determine the sort order of suggestions. The default provider has an suggestion priority of 1
filterSuggestions
(optional): If set to true
, autocomplete+
will perform fuzzy filtering and sorting on the list of matches returned by getSuggestions
.dispose
(optional): Will be called if your provider is being destroyed by autocomplete+
onDidInsertSuggestion
(optional): Function that is called when a suggestion from your provider was inserted into the buffer editor
: the TextEditor your suggestion was inserted intriggerPosition
: A Point where autocomplete was triggeredsuggestion
: The suggestion object that was inserted.Some providers satisfy a suggestion request in an asynchronous way (e.g. it may need to dispatch requests to an external process to get suggestions). To asynchronously provide suggestions, simply return a Promise
from your getSuggestions
:
getSuggestions: (options) ->
+ return new Promise (resolve) ->
+ # Build your suggestions here asynchronously...
+ resolve(suggestions) # When you are done, call resolve and pass your suggestions to it
+
An options
object will be passed to your getSuggestions
function, with the following properties:
editor
: The current TextEditor
bufferPosition
: The position of the cursorscopeDescriptor
: The scope descriptor for the current cursor positionprefix
: The word characters immediately preceding the current cursor positionactivatedManually
: Whether the autocomplete request was initiated by the user (e.g. with ctrl+space)provider =
+ selector: '.source.js, .source.coffee'
+ disableForSelector: '.source.js .comment'
+
+ getSuggestions: ({editor, bufferPosition, scopeDescriptor, prefix}) ->
+ new Promise (resolve) ->
+ # Find your suggestions here
+ suggestion =
+ text: 'someText' # OR
+ snippet: 'someText(${1:myArg})'
+ displayText: 'someText' # (optional)
+ replacementPrefix: 'so' # (optional)
+ type: 'function' # (optional)
+ leftLabel: '' # (optional)
+ leftLabelHTML: '' # (optional)
+ rightLabel: '' # (optional)
+ rightLabelHTML: '' # (optional)
+ className: '' # (optional)
+ iconHTML: '' # (optional)
+ description: '' # (optional)
+ descriptionMoreURL: '' # (optional)
+ characterMatchIndices: [0, 1, 2] # (optional)
+ resolve([suggestion])
+
Your suggestions should be returned from getSuggestions
as an array of objects with the following properties:
text
(required; or snippet
): The text which will be inserted into the editor, in place of the prefixsnippet
(required; or text
): A snippet string. This will allow users to tab through function arguments or other options. e.g. 'myFunction(${1:arg1}, ${2:arg2})'
. See the snippets package for more information.displayText
(optional): A string that will show in the UI for this suggestion. When not set, snippet || text
is displayed. This is useful when snippet
or text
displays too much, and you want to simplify. e.g. {type: 'attribute', snippet: 'class="$0"$1', displayText: 'class'}
replacementPrefix
(optional): The text immediately preceding the cursor, which will be replaced by the text
. If not provided, the prefix passed into getSuggestions
will be used.type
(optional): The suggestion type. It will be converted into an icon shown against the suggestion. screenshot. Predefined styles exist for variable
, constant
, property
, value
, method
, function
, class
, type
, keyword
, tag
, snippet
, import
, require
. This list represents nearly everything being colorized.leftLabel
(optional): This is shown before the suggestion. Useful for return values. screenshotleftLabelHTML
(optional): Use this instead of leftLabel
if you want to use html for the left label.rightLabel
(optional): An indicator (e.g. function
, variable
) denoting the "kind" of suggestion this representsrightLabelHTML
(optional): Use this instead of rightLabel
if you want to use html for the right label.className
(optional): Class name for the suggestion in the suggestion list. Allows you to style your suggestion via CSS, if desirediconHTML
(optional): If you want complete control over the icon shown against the suggestion. e.g. iconHTML: '<i class="icon-move-right"></i>'
screenshot. The background color of the icon will still be determined (by default) from the type
.description
(optional): A doc-string summary or short description of the suggestion. When specified, it will be displayed at the bottom of the suggestions list.descriptionMoreURL
(optional): A url to the documentation or more information about this suggestion. When specified, a More..
link will be displayed in the description area. characterMatchIndices
(optional): A list of indexes where the characters in the prefix appear in this suggestion's text. e.g. "foo" in "foo_bar" would be [0, 1, 2]
.autocomplete+
In your package.json
, add:
"providedServices": {
+ "autocomplete.provider": {
+ "versions": {
+ "4.0.0": "provide"
+ }
+ }
+}
+
Then, in your main.coffee
(or whatever file you define as your main
in package.json
i.e. "main": "./lib/your-main"
would imply your-main.coffee
), add the following:
For a single provider:
module.exports =
+ provide: -> @yourProviderHere
+
For multiple providers, just return an array:
module.exports =
+ provide: -> [@yourProviderHere, @yourOtherProviderHere]
+
We've taken to making each provider its own clean repo:
Check out the lib directory in each of these for the code!
The prefix
passed to getSuggestions
may not be sufficient for your language. You may need to generate your own prefix. Here is a pattern to use your own prefix:
provider =
+ selector: '.source.js, .source.coffee'
+
+ getSuggestions: ({editor, bufferPosition}) ->
+ prefix = @getPrefix(editor, bufferPosition)
+
+ new Promise (resolve) ->
+ suggestion =
+ text: 'someText'
+ replacementPrefix: prefix
+ resolve([suggestion])
+
+ getPrefix: (editor, bufferPosition) ->
+ # Whatever your prefix regex might be
+ regex = /[\w0-9_-]+$/
+
+ # Get the text for the line up to the triggered buffer position
+ line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition])
+
+ # Match the regex to the line, and return the match
+ line.match(regex)?[0] or ''
+
Note
Please note that its possible this is outdated, as its original version was published by @'Phil' on Jan 24, 2017.
The builtin SymbolProvider
allows showing the types of symbols in the suggestion list.
The icon colors are intended to match the syntax color of the symbol type. e.g. functions are blue in this theme, so the function icon is also blue.
Each language can tune how the SymbolProvider
tags symbols by modifying the config of the language package.
# An example for the CoffeeScript language
+'.source.coffee':
+ autocomplete:
+ symbols:
+ class:
+ selector: '.class.name, .inherited-class, .instance.type'
+ typePriority: 4
+ function:
+ selector: '.function.name'
+ typePriority: 3
+ variable:
+ selector: '.variable'
+ typePriority: 2
+ '': # the catch-all
+ selector: '.source'
+ typePriority: 1
+ builtin:
+ suggestions: [
+ 'one'
+ 'two'
+ ]
+
A more generic example:
+'.source.language':
+ autocomplete:
+ symbols:
+ typename1:
+ selector: '.some, .selectors'
+ typePriority: 1
+ typename2:
+ suggestions: [...]
+ typename3:
+ ...
+
Any number of Typename objects are supported, and typename
can be anything you choose. However, you are encouraged to use one of the predefined typenames
. There are predefined styles for the following types:
builtin
+class
+constant
+function
+import
+keyword
+method
+module
+mixin
+package
+property
+require
+snippet
+tag
+type
+value
+variable
+
Typename objects support the following properties:
selector
: The selector that matches your symbol types. e.g. '.variable.name'
. You can also have several selectors separated by commas, just like in CSS '.variable.name, .storage.variable'
typePriority
: The priority this Typename object has over others. e.g. in our CoffeeScript example above, if a symbol is tagged with the function
type in one part of the code, but class
in another part of the code, it will be displayed to the user as a class
because class
has a higher typePriority
suggestions
: This allows you to specify explicit completions for some scope. A good use is builtins: e.g. ['Math', 'Array', ...]
in JavaScript suggestions
list can also objects using any of the properties from the provider API.Coming up with the selectors for a given Typename object might be the hardest part of defining the autocomplete.symbols
config. Fortunately there is a tool to help
Open the command palette, then search for log cursor scope
. You will be presented with a blue box like the following:
Each bullet in the box is a node. The last bullet is the symbol itself, and each preceding line is a parent of the symbol — just like CSS. With this information, you can see that the symbol can be matched with several selectors: '.variable'
, '.variable.assignment'
, '.source.coffee .variable'
, etc.
Welcome to the github
wiki!
This wiki contains a single file from the original upstream Atom Wiki. Which while serves no purpose for the current mission or direction of Pulsar, just seemed like an interesting insight into how Atom originally began, and how progress originally happened.
Note
Please note that it's possible this is outdated, as its original version was published by @'Michelle Tilley' on Jun 7, 2017.
Note
Please note that its possible this is outdated, as its original version was published by @'Katrina Uychaco' on Jun 14, 2017.
core:undo
support Note
Please note that its possible this is outdated, as its original version was published by @'Damein Guard' on Feb 16, 2018.
In order to properly analyze Java files a project definition must be found that indicates the packages, paths to search for classes etc.
You should open the folder that contains the pom.xml or build.gradle file if you want full diagnostics, errors, auto-completion etc.
If you don't need the full analysis you can ignore this warning and work with a subset of the available features.
Welcome to the ide-java
wiki!
This wiki contains a single file from the original upstream Atom Wiki. That may mean parts of it are out of date, but otherwise hopefully it is a helpful resource relating to our IDE-Java Repo!
This aims to be a collection of information about the first-party packages published for Pulsar.
Secondly, and currently, it is an archive of every Wiki that was published from the upstream Atom Organization.
This means some of this information may be old, or no longer relevant. If it is, it has been marked as so at the top of the file under a warning.
None-the-less the information can still be helpful, and may prove to be valuable. Which is why it has been archived here.
This aims to be a collection of information about the packages published for the Pulsar Editor. Currently, anything in Core Packages is an archive of every Wiki that was published from the upstream Atom Organization.
This means some of this information may be old, or no longer relevant. If it is, it has been marked as so at the top of the file under a warning. None-the-less the information can still be helpful, and may prove to be valuable. Which is why it has been archived here.
As well, we have a record of community packages as well. This is meant to be a record of these. This section should be updated as we get more community contributions of these. The process for adding this will be added at a later date.
There are currently thousands of community packages published to the Pulsar Package Registry, if you are coming from Atom you should be able to find all of your familiar packages as we have archived everything that Atom once had published.
If one of your favorite packages that you had on Atom is missing feel free to contact us or take a look through the Pulsar Package Registry Administrative Actions Log to see if it's been officially removed.
Otherwise to browse the vast collection of packages available to install on Pulsar you can open up the Pulsar Settings Package page, or browse them on the Pulsar Package Registry Website.
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment include:
Examples of unacceptable behavior by participants include:
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team using our Discord Server. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://contributor-covenant.org/version/1/4
A collection of miscellaneous resources around Pulsar and the project.
A list of terms and their meanings used by the application and community.
The Pulsar API reference.
A list of tools and services used by the Pulsar team and information about them.
The Pulsar team greatly values your, and everybody's, privacy. We therefore keep any collection of personal data to an absolute minimum, but what little is collected across the entirety of the Pulsar Project will be detailed here.
Also please note that this Privacy Policy excludes any Community Packages that may be installed as those are created and maintained by a third party.
At the bottom of each service listed in this document there will be a "TLDR" to quickly sum up all data that is collected, how it's used, and who can use it.
Date of the Last Change to the Privacy Policy: 2022, December 10th
https://api.pulsar-edit.dev
)The Pulsar Backend is the service you connect to when using all Pulsar Packages on the web via https://web.pulsar-edit.dev
and when using the built in Pulsar Settings View or Package Manager to interact with community packages.
All actions are subject to this service's Privacy Policy, including browsing, publishing, installing, deleting, starring, or unstarring packages.
Over the course of using the Pulsar Backend the following personal details are collected about you, and used as described below.
When you send a request of any kind to the Pulsar Backend this information is made available to us through the web request itself and the User Agent of the request.
The only individuals that have access to the above details are Pulsar's Core Admin Team in charge of the Pulsar Backend.
The logs that contain the above information is kept in the cloud for 30 days before it is automatically deleted.
When you create a user account with Pulsar to star, or publish packages, you provide some details to Pulsar to create the account. It should be made explicit that Pulsar does not store any type of credentials your account at any time.
node_id
(Think of your node_id
like the random number assigned to you when making a user account. This is public information)When you sign up with Pulsar, and choose to make an account, this is the data collected to make sure your account works, and is able to protect Packages from Malicious Actors pretending to be someone they're not.
This information is used to let you authenticate against the Pulsar Backend when publishing, deleted, or starring a package.
The only people who have access to this information are Pulsar's Core Admin Team in charge of the Backend.
The above details will be kept until either you request deletion of your account, or there is a built in supported method to delete your Pulsar User Account.
To request removal of the above details feel free to contact the Pulsar Core Admin Team, for quicker response times, contact use through the Pulsar Discord. Or you can contact us through any of the supported methods, or even via email to admin@pulsar-edit.dev
node_id
.https://pulsar-edit.dev
)The Pulsar Website is a service you connect to anytime you visit our website.
This Website is hosted on GitHub Pages which while conveinent for us, also means that we have no built in mechanism to access any personal data of any kind through this service. That is to say we don't; no analytics are set up on the website, and no extra code is run on the website to collect any data.
The main Pulsar Application, available here is the program you use anytime you launch Pulsar, or edit text within it.
While Atom the original implementation of the Editor did collect telemetry and metrics about the users, when we began work on the editor one of the first things that we removed was the telemetry.
That is to say now, the Pulsar Editor collects Zero information about its users.
Under Construction
This document is under construction, please check back soon for updates. Please see our socials and feel free to ask for assistance or inquire as to the status of this document.
Here you will find a list of tools used by the Pulsar team and information about them.
Cirrus CI is used for Pulsar's continuous integration as well as for building application binaries.
Codacy is used to scan committed code for any issues that may have been missed.
Currently though Codacy is only used on the following repositories:
Crowdin will be used for Pulsar's internationalization efforts but exact details on this are still pending.
While most repositories you can easily tell what Package Manager is being used by checking for a specific lock file, there are some execptions to this that should be noted.
yarn
as its Package Manager.pnpm
as its Package Manager.The package-backend currently uses DigitalOcean to host the PostgreSQL Pulsar Package Repositories data in the cloud.
Both the package-backend and package-frontend use Google App Engine to host the compute instance of these websites in the cloud.
Pulsar Dependency Tester is a GitHub action used to test changes of a core Pulsar Dependency if you need to determine how your core package dependency will run on the current version of the Pulsar Editor.
This GitHub Action is a direct Pulsar replacement of the previous Action to test Atom Dependencies Setup Atom.
Here is all the information on how the Pulsar website is built and configured.
The website itself is built using VuePress v2 and the Vuepress Hope Theme.
See the documentation there for generic items not covered in this guide.
corepack enable
The website repository is https://github.com/pulsar-edit/pulsar-edit.github.io. Other assets are stored on other repositories but these will be downloaded automatically.
git clone https://github.com/pulsar-edit/pulsar-edit.github.io
+
+cd pulsar-edit.github.io
+
+pnpm install
+
Once installed there are a number of scripts available to help you develop and build the site. Just prefix each command with pnpm
. e,g, pnpm dev
.
dev
Starts a watch task that will rebuild VuePress whenever a change has been made to the included Markdown and JavaScript files. Additionally, it launches the development server to test the results in the browser.
build
Creates an optimized production build.
format
Runs Prettier over all Markdown files included in the repository to ensure consistent formatting.
Note: This task will run automatically on every commit, so it can be ignored in most cases
lint
Lints all Markdown files in the repository.
Note: This task will run automatically on every commit, so it can be ignored in most cases
Whilst dev
does run a watch task, not everything will be updated and some changes will require you to shut down the server and start it again. For example adding @include
files to another file will not rebuild automatically.
If you wish to add new files from another repository via alias or @include
then you will need to run pnpm update
to get the latest version of the repository - the pnpm-lock.yml
file will also be updated and must be part of any commit.
Nearly everything regarding the configuration of the website itself is controlled via the files found in the /docs/.vuepress
directory.
Currently we have three main configuration files.
config.js
This is the main configuration file for the website. This controls everything from the available settings, additional VuePress plugins, website description and various other elements to control various settings and plugins.
For a full reference you can look at the documentation for VuePress and the Hope Theme.
This file is broken down to to keep it tidy, the below files are imported to config.js
to extend the configuration file without making it unwieldy.
navbar.js
This controls the layout for the links in the top middle of the page and is always displayed.
Items that go here are ones that we always want to be shown and should always be available for quick navigation.
Each object can have a number of different values. The main ones we use are:
text
: This sets the text for the label.icon
: Used to prefix an icon to the item. The theme supports the free FontAwesome font natively. To add an icon you need to specify its name without the first fa-
e.g. fa-house would be specified as solid fa-house
.link
: This controls where the link will actually take you. This can be a relative reference internal to the website or can be a URL to an external site.children
: Allows you to specify an array of child objects which will appear as a dropdown on mouseover. Use of this disables the link
value. Each child can be defined as a full object as described here or can simply be a relative link from which the text will be set by the YAML title.For a full reference you can look at the documentation for VuePress and the Hope Theme.
sidebar.js
This control what is displayed in the sidebar on the left of the website. It is not displayed globally, only on directories which are set within the sidebar, currently we only have docs
configured.
Like navbar.js
each sidebare item is configured as an object with a number of different values.
text
: This sets the text for the label.link
: Controls the relative link for navigating the documents within the sectionicon
: Used to prefix an icon to the item. The theme supports the free FontAwesome font natively. To add an icon you need to specify its name without the first fa-
e.g. fa-house would be specified as solid fa-house
.prefix
: This adds a file path prefix to the item so its children do not need to specify the full path.collapsable
: (sic) Controls whether the item can be collapsed. Note: This a breaking change on a future version of the Hope Theme so will need to be renamed collapsible
when updated, see: Issue.children
: Takes an array of objects configured as above. Can also be set as a simple relative link in which case the title will be the YAML title of the document it links to.For a full reference you can look at the documentation for VuePress and the Hope Theme.
Within the styles/
directory you will find the .scss file for controlling various aspects of the website's theme.
TODO: This will be updated when we actually modify these files extensively.
One of the most important things to take note of when adding new documentation is where it should go within the website layout.
The generalized overall layout of the website looks like this:
docs
+├── root level .md files
+└── section folder
+ ├── sections
+ └── index.md
+
The general idea is that for files that can stand by themselves (for example the About Us
page, Repositories
etc.) they exist at the docs/
"root" level.
For anything that is more complex it needs to have a section directory named appropriately, an index.md
file within it and a sections
directory.
index.md
This index file needs to have a YAML frontmatter to define, at a minimum, the title of the document. This is displayed as an H1
header for the page (note: subsequent H1
headers will be ignored so always start at H2
).
The rest of this index file will be used to display the actual content you want to show. This is done in a number of ways.
First of all you can just include standard markdown. This is often used for introducing the section or adding one of our reusable components (e.g. a danger
container).
The rest of the file should consist of @includes
which take data from other folders on the website and integrates it automatically. Usually this will be the sections
files which will be covered next.
e.g.
@include(sections/file-organization.md)
+
However you can also use @include
to feature files from a different section of the website or even files from outside the main site. We use this to include files which are maintained on the organization .github repo for org-level documents.
This is done by having a value defined on the config.js
file which will provide an alias for us to use:
if (file.startsWith("@orgdocs")) {
+ return file.replace(
+ "@orgdocs",
+ path.resolve(__dirname, "../../node_modules/.github/")
+ );
+}
+
This allows us to include org-level docs by using this special alias.
e.g.
@include(@orgdocs/TOOLING.md)
+
The sections
directory is where we include the rest of the documents broken down by section. These should be self contained files which can be used alone but are designed to be included on the section page. This approach allows us flexibility with ordering as well as including these files in other places without needing to duplicate the material.
Files here can be navigated to directly on the website but should not be linked to directly.
These files shoud not have any YAML frontmatter as they will be included and shown as text.
Assets should be uploaded to the .github repo repository so they can be used org-wide.
An alias for this exists in config.js
to access files from this repository.
alias: {
+ '@images': path.resolve(__dirname, '../../node_modules/.github/images')
+},
+
So to include an image you simply need to use the standard markdown image link along with the alias:
e.g.
![MyImage](@images/path/to/image.png)
+
The documentation for the project should maintain a consistent style where possible. This covers a number of aspects such as writing style, naming conventions, folder structure, links etc.
All docs are currently in American English (en-US) but localization is planned.
The main structure for documentation can be seen in docs/docs
. There are a number of "root" sections:
launch-manual
- For the current main Pulsar documentationpackages
- Currently holds wiki info from the atom package reposresources
- For other referenced docsblog
- For the website blogatom-archive
- For "as is" archived Atom documentationWithin each section is an index.md
which will usually contain info about each sub-section as well as links to them. These correspond to the second level items on the sidebar.
Inside sections
are the sub-sections which group more specific topics. These also have an index.md
which corresponds to the third level item on the sidebar. This file is displayed on the website as a single long documenent but is actually created from a number of @include() lines which reference individual sections within the next sections directory. These should be relative to the location of index.md e.g. @include(sections/pulsar-packages.md). This file also contains the frontmatter for defining the title, language and description of the file and should also be the first level heading for the page. Here is also where you can place a container such as Under Construction
to apply to the entire page.
Inside the next sections
directory should be the actual content of the document. Each section should start with a second level header and should not contain any frontmatter.
Internal links can just be to the header (e.g.[Structure](#structure)
), this to all sections included on the parent index.md
so care should be made to not create any duplicate headers.
All other links should be relative but do not need to reference the index file itself (e.g.[Installing](../getting-started#installing)
) will find the heading #installing
within the index file in the getting-started
directory above.
Images should be added to [pulsar-assets](https://github.com/pulsar-edit/pulsar-assets)
and referenced from the package imported from it. This is done via an alias on the .vuepress/config.js
file which adds most of the path for you: '@images': path.resolve(__dirname, '../../node_modules/pulsar-assets/images')
so the link to your image would just be ![myImage](@images/pulsar/myImage.png "My Image")
.
The name of the application is Pulsar
and should be capitalized as such. Whilst the website and GitHub org name is Pulsar-Edit
, this should not be used within documentation outside of links to the GitHub org or website.
Operating systems should be named as such:
Linux
- All GNU/Linux distributionsmacOS
- Apple's current operating system familyWindows
- Microsoft Windows family of operating systemsThis is also the order they should appear in within the tab switcher.
When using the #tabs
switcher they should be in this order.
When referencing them inline then they should be abbreviated to the following, strongly emphasized and separated by a -
:
To keep order consistent it should be LNX -> MAC -> WIN. If instructions: common to two then it should either be LNX/MAC, LNX/WIN -> MAC/WIN
For Linux we may sometimes need to reference particular distros or families of distributions. We currently use:
Ubuntu/Debian
for all distributions based on Debian or UbuntuFedora/RHEL
for all distrububtions based on Fedora Linux & Red Hat Red Hat Enterprise Linux. This includes AlmaLinux, CentOS, Rocky Linux etc.Arch
- for all Arch based distributions such as Manjaro, Garuda, ArcoLinux etc.OpenSUSE
- for all OpenSUSE based distributions such as GeckoLinuxWe may need to add more in the future but generally users of less popular or more technical distributions such as Gentoo or NixOS understand how to adapt to their OS from the instructions above.
Where you want to display an info
, warning
or tab/code switcher in the document you should use a container with the :::
syntax.
e.g.
::: tabs#filename
+
+@tab Linux
+
+Lorem ipsum dolor sit amet...
+
+@tab macOS
+
+Lorem ipsum dolor sit amet...
+
+@tab Windows
+
+Lorem ipsum dolor sit amet...
+
+:::
+
or
::: tip My Helpful Tip
+
+You might want to do X to get Y
+
+:::
+
You can also find a list of currently maintained preformatted containers for various purposes at pulsar-edit.github.io/common-text-blocks.md.
See VuePress Hope documentation
TODO: Needs consensus
This is a guide on how to add a blog post to the website which will be shown on https://pulsar-edit.dev/article/.
We are using the Vuepress Blog Plugin which comes as part of our Vuepress Hope Theme with some light configuration to suit our purposes.
This is all implemented in the main website repository.
YYYYMMDD-<author>-<title>.md
e.g 20221031-CreativeUsername-ThisIsMyBlogPost.md
title
, author
, date
, category
and tag
as the minimum as the others will default to false. title
- String: The displayed title of the post, consider this as H1
author
- String: The name of the author to be displayed.date
- Date (ISO 8601): Allows display of date as well as enabling sorting on the timeline, set to the same as your filename date but with hyphens (e.g. 2022-10-31
).category
- String (multiline): Enables filtering by category, this should be based on the subject of the post e.g. release
, dev log
, announcement
. This is a multiline field if you want to set more than one category.tag
- String (multiline): Enables filtering by tags, this should be based on the content of the post and areas it touches on e.g. website
, editor
, config
.sticky
- Bool: Enables "pinning" on thestar
- Bool: Enables use of the star
category for any important articles we want to remain visible.article
- Bool: You probably won't want to use this but setting this to false
will exclude this page from appearing. This is set on the "example" blog post intentionally.<!-- more -->
. Anything above the comment will be treated as the excerpt and anything underneath will be the content of the post. ![myImage](./assets/myImage.png)
) and should be located in the blog/assets
directory. You may also use standard html <img>
tags, particularly if you wish to control the displayed size of the image.See example post with everything above.
See building
The website itself has a number of features which the aforementioned frontmatter fields can influence.
There is a "selector" at the top of the site which currently reads All
, Star
, Slides
, Encrypted
. The only ones we use are the first two and currently the theme doesn't support configuration of this. If necessary we may be able to hide it with CSS.
Any article with Star
set to true
will be shown in the star
category.
On the right bar are 4 counters/links and filters for Articles
, Category
, Tag
and Timeline
.Category
and Tag
are used to filter articles depending on the categories submitted in the respective frontmatter fields. The Timeline
allows views of blog posts over time according to the dates set in the date
frontmatter field.
Just ask in Discord or GH Discussions and ping the @documentation
team.
This is very much barebones default config so please let us know if you have any suggestions as to how we can improve it.
This is a guide on how to add a blog post to the website which will be shown on https://pulsar-edit.dev/article/.
We are using the Vuepress Blog Plugin which comes as part of our Vuepress Hope Theme with some light configuration to suit our purposes.
This is all implemented in the main website repository.
YYYYMMDD-<author>-<title>.md
e.g 20221031-CreativeUsername-ThisIsMyBlogPost.md
title
, author
, date
, category
and tag
as the minimum as the others will default to false. title
- String: The displayed title of the post, consider this as H1
author
- String: The name of the author to be displayed.date
- Date (ISO 8601): Allows display of date as well as enabling sorting on the timeline, set to the same as your filename date but with hyphens (e.g. 2022-10-31
).category
- String (multiline): Enables filtering by category, this should be based on the subject of the post e.g. release
, dev log
, announcement
. This is a multiline field if you want to set more than one category.tag
- String (multiline): Enables filtering by tags, this should be based on the content of the post and areas it touches on e.g. website
, editor
, config
.sticky
- Bool: Enables "pinning" on thestar
- Bool: Enables use of the star
category for any important articles we want to remain visible.article
- Bool: You probably won't want to use this but setting this to false
will exclude this page from appearing. This is set on the "example" blog post intentionally.<!-- more -->
. Anything above the comment will be treated as the excerpt and anything underneath will be the content of the post. ![myImage](./assets/myImage.png)
) and should be located in the blog/assets
directory. You may also use standard html <img>
tags, particularly if you wish to control the displayed size of the image.See example post with everything above.
See building
The website itself has a number of features which the aforementioned frontmatter fields can influence.
There is a "selector" at the top of the site which currently reads All
, Star
, Slides
, Encrypted
. The only ones we use are the first two and currently the theme doesn't support configuration of this. If necessary we may be able to hide it with CSS.
Any article with Star
set to true
will be shown in the star
category.
On the right bar are 4 counters/links and filters for Articles
, Category
, Tag
and Timeline
.Category
and Tag
are used to filter articles depending on the categories submitted in the respective frontmatter fields. The Timeline
allows views of blog posts over time according to the dates set in the date
frontmatter field.
Just ask in Discord or GH Discussions and ping the @documentation
team.
This is very much barebones default config so please let us know if you have any suggestions as to how we can improve it.
corepack enable
The website repository is https://github.com/pulsar-edit/pulsar-edit.github.io. Other assets are stored on other repositories but these will be downloaded automatically.
git clone https://github.com/pulsar-edit/pulsar-edit.github.io
+
+cd pulsar-edit.github.io
+
+pnpm install
+
Once installed there are a number of scripts available to help you develop and build the site. Just prefix each command with pnpm
. e,g, pnpm dev
.
dev
Starts a watch task that will rebuild VuePress whenever a change has been made to the included Markdown and JavaScript files. Additionally, it launches the development server to test the results in the browser.
build
Creates an optimized production build.
format
Runs Prettier over all Markdown files included in the repository to ensure consistent formatting.
Note: This task will run automatically on every commit, so it can be ignored in most cases
lint
Lints all Markdown files in the repository.
Note: This task will run automatically on every commit, so it can be ignored in most cases
Whilst dev
does run a watch task, not everything will be updated and some changes will require you to shut down the server and start it again. For example adding @include
files to another file will not rebuild automatically.
If you wish to add new files from another repository via alias or @include
then you will need to run pnpm update
to get the latest version of the repository - the pnpm-lock.yml
file will also be updated and must be part of any commit.
Nearly everything regarding the configuration of the website itself is controlled via the files found in the /docs/.vuepress
directory.
Currently we have three main configuration files.
config.js
This is the main configuration file for the website. This controls everything from the available settings, additional VuePress plugins, website description and various other elements to control various settings and plugins.
For a full reference you can look at the documentation for VuePress and the Hope Theme.
This file is broken down to to keep it tidy, the below files are imported to config.js
to extend the configuration file without making it unwieldy.
navbar.js
This controls the layout for the links in the top middle of the page and is always displayed.
Items that go here are ones that we always want to be shown and should always be available for quick navigation.
Each object can have a number of different values. The main ones we use are:
text
: This sets the text for the label.icon
: Used to prefix an icon to the item. The theme supports the free FontAwesome font natively. To add an icon you need to specify its name without the first fa-
e.g. fa-house would be specified as solid fa-house
.link
: This controls where the link will actually take you. This can be a relative reference internal to the website or can be a URL to an external site.children
: Allows you to specify an array of child objects which will appear as a dropdown on mouseover. Use of this disables the link
value. Each child can be defined as a full object as described here or can simply be a relative link from which the text will be set by the YAML title.For a full reference you can look at the documentation for VuePress and the Hope Theme.
sidebar.js
This control what is displayed in the sidebar on the left of the website. It is not displayed globally, only on directories which are set within the sidebar, currently we only have docs
configured.
Like navbar.js
each sidebare item is configured as an object with a number of different values.
text
: This sets the text for the label.link
: Controls the relative link for navigating the documents within the sectionicon
: Used to prefix an icon to the item. The theme supports the free FontAwesome font natively. To add an icon you need to specify its name without the first fa-
e.g. fa-house would be specified as solid fa-house
.prefix
: This adds a file path prefix to the item so its children do not need to specify the full path.collapsable
: (sic) Controls whether the item can be collapsed. Note: This a breaking change on a future version of the Hope Theme so will need to be renamed collapsible
when updated, see: Issue.children
: Takes an array of objects configured as above. Can also be set as a simple relative link in which case the title will be the YAML title of the document it links to.For a full reference you can look at the documentation for VuePress and the Hope Theme.
Within the styles/
directory you will find the .scss file for controlling various aspects of the website's theme.
TODO: This will be updated when we actually modify these files extensively.
The documentation for the project should maintain a consistent style where possible. This covers a number of aspects such as writing style, naming conventions, folder structure, links etc.
All docs are currently in American English (en-US) but localization is planned.
The main structure for documentation can be seen in docs/docs
. There are a number of "root" sections:
launch-manual
- For the current main Pulsar documentationpackages
- Currently holds wiki info from the atom package reposresources
- For other referenced docsblog
- For the website blogatom-archive
- For "as is" archived Atom documentationWithin each section is an index.md
which will usually contain info about each sub-section as well as links to them. These correspond to the second level items on the sidebar.
Inside sections
are the sub-sections which group more specific topics. These also have an index.md
which corresponds to the third level item on the sidebar. This file is displayed on the website as a single long documenent but is actually created from a number of @include() lines which reference individual sections within the next sections directory. These should be relative to the location of index.md e.g. @include(sections/pulsar-packages.md). This file also contains the frontmatter for defining the title, language and description of the file and should also be the first level heading for the page. Here is also where you can place a container such as Under Construction
to apply to the entire page.
Inside the next sections
directory should be the actual content of the document. Each section should start with a second level header and should not contain any frontmatter.
Internal links can just be to the header (e.g.[Structure](#structure)
), this to all sections included on the parent index.md
so care should be made to not create any duplicate headers.
All other links should be relative but do not need to reference the index file itself (e.g.[Installing](../getting-started#installing)
) will find the heading #installing
within the index file in the getting-started
directory above.
Images should be added to [pulsar-assets](https://github.com/pulsar-edit/pulsar-assets)
and referenced from the package imported from it. This is done via an alias on the .vuepress/config.js
file which adds most of the path for you: '@images': path.resolve(__dirname, '../../node_modules/pulsar-assets/images')
so the link to your image would just be ![myImage](@images/pulsar/myImage.png "My Image")
.
The name of the application is Pulsar
and should be capitalized as such. Whilst the website and GitHub org name is Pulsar-Edit
, this should not be used within documentation outside of links to the GitHub org or website.
Operating systems should be named as such:
Linux
- All GNU/Linux distributionsmacOS
- Apple's current operating system familyWindows
- Microsoft Windows family of operating systemsThis is also the order they should appear in within the tab switcher.
When using the #tabs
switcher they should be in this order.
When referencing them inline then they should be abbreviated to the following, strongly emphasized and separated by a -
:
To keep order consistent it should be LNX -> MAC -> WIN. If instructions: common to two then it should either be LNX/MAC, LNX/WIN -> MAC/WIN
For Linux we may sometimes need to reference particular distros or families of distributions. We currently use:
Ubuntu/Debian
for all distributions based on Debian or UbuntuFedora/RHEL
for all distrububtions based on Fedora Linux & Red Hat Red Hat Enterprise Linux. This includes AlmaLinux, CentOS, Rocky Linux etc.Arch
- for all Arch based distributions such as Manjaro, Garuda, ArcoLinux etc.OpenSUSE
- for all OpenSUSE based distributions such as GeckoLinuxWe may need to add more in the future but generally users of less popular or more technical distributions such as Gentoo or NixOS understand how to adapt to their OS from the instructions above.
Where you want to display an info
, warning
or tab/code switcher in the document you should use a container with the :::
syntax.
e.g.
::: tabs#filename
+
+@tab Linux
+
+Lorem ipsum dolor sit amet...
+
+@tab macOS
+
+Lorem ipsum dolor sit amet...
+
+@tab Windows
+
+Lorem ipsum dolor sit amet...
+
+:::
+
or
::: tip My Helpful Tip
+
+You might want to do X to get Y
+
+:::
+
You can also find a list of currently maintained preformatted containers for various purposes at pulsar-edit.github.io/common-text-blocks.md.
See VuePress Hope documentation
TODO: Needs consensus
One of the most important things to take note of when adding new documentation is where it should go within the website layout.
The generalized overall layout of the website looks like this:
docs
+├── root level .md files
+└── section folder
+ ├── sections
+ └── index.md
+
The general idea is that for files that can stand by themselves (for example the About Us
page, Repositories
etc.) they exist at the docs/
"root" level.
For anything that is more complex it needs to have a section directory named appropriately, an index.md
file within it and a sections
directory.
index.md
This index file needs to have a YAML frontmatter to define, at a minimum, the title of the document. This is displayed as an H1
header for the page (note: subsequent H1
headers will be ignored so always start at H2
).
The rest of this index file will be used to display the actual content you want to show. This is done in a number of ways.
First of all you can just include standard markdown. This is often used for introducing the section or adding one of our reusable components (e.g. a danger
container).
The rest of the file should consist of @includes
which take data from other folders on the website and integrates it automatically. Usually this will be the sections
files which will be covered next.
e.g.
+File not found
+
+
However you can also use @include
to feature files from a different section of the website or even files from outside the main site. We use this to include files which are maintained on the organization .github repo for org-level documents.
This is done by having a value defined on the config.js
file which will provide an alias for us to use:
if (file.startsWith("@orgdocs")) {
+ return file.replace(
+ "@orgdocs",
+ path.resolve(__dirname, "../../node_modules/.github/")
+ );
+}
+
This allows us to include org-level docs by using this special alias.
e.g.
@include-push(/home/runner/work/pulsar-edit.github.io/pulsar-edit.github.io/node_modules/.github)
+# Pulsar tooling
+
+Here you will find a list of tools used by the Pulsar team and information about them.
+
+## Continuous Integration
+
+### [Cirrus CI](https://cirrus-ci.com/github/pulsar-edit/pulsar)
+
+Cirrus CI is used for Pulsar's continuous integration as well as for building application binaries.
+
+### [Codacy](https://app.codacy.com/gh/pulsar-edit/repositories)
+
+Codacy is used to scan committed code for any issues that may have been missed.
+
+Currently though Codacy is only used on the following repositories:
+
+* [ppm](https://app.codacy.com/gh/pulsar-edit/ppm/dashboard)
+* [pulsar](https://app.codacy.com/gh/pulsar-edit/pulsar/dashboard)
+* [background-tips](https://app.codacy.com/gh/pulsar-edit/background-tips/dashboard)
+* [autocomplete-plus](https://app.codacy.com/gh/pulsar-edit/autocomplete-plus/dashboard)
+
+## i18n (Internationalization)
+
+### [Crowdin](https://crowdin.pulsar-edit.dev/)
+
+Crowdin will be used for Pulsar's internationalization efforts but exact details on this are still pending.
+
+## Package Managers
+
+While most repositories you can easily tell what Package Manager is being used by checking for a specific lock file, there are some execptions to this that should be noted.
+
+* [pulsar](https://github.com/pulsar-edit/pulsar) uses `yarn` as its Package Manager.
+* [pulsar-edit.github.io](https://github.com/pulsar-edit/pulsar-edit.github.io) uses `pnpm` as its Package Manager.
+
+## Cloud Database
+
+The [package-backend](https://github.com/pulsar-edit/package-backend) currently uses DigitalOcean to host the PostgreSQL Pulsar Package Repositories data in the cloud.
+
+## Cloud Compute
+
+Both the [package-backend](https://github.com/pulsar-edit/package-backend) and [package-frontend](https://github.com/pulsar-edit/package-frontend) use Google App Engine to host the compute instance of these websites in the cloud.
+
+## Additional Testing tools
+
+### [Action Pulsar Dependency Tester](https://github.com/marketplace/actions/action-pulsar-dependency-tester)
+
+Pulsar Dependency Tester is a GitHub action used to test changes of a core Pulsar Dependency if you need to determine how your core package dependency will run on the current version of the Pulsar Editor.
+
+This GitHub Action is a direct Pulsar replacement of the previous Action to test Atom Dependencies [Setup Atom](https://github.com/marketplace/actions/setup-atom).
+
+@include-pop()
+
+
The sections
directory is where we include the rest of the documents broken down by section. These should be self contained files which can be used alone but are designed to be included on the section page. This approach allows us flexibility with ordering as well as including these files in other places without needing to duplicate the material.
Files here can be navigated to directly on the website but should not be linked to directly.
These files shoud not have any YAML frontmatter as they will be included and shown as text.
Assets should be uploaded to the .github repo repository so they can be used org-wide.
An alias for this exists in config.js
to access files from this repository.
alias: {
+ '@images': path.resolve(__dirname, '../../node_modules/.github/images')
+},
+
So to include an image you simply need to use the standard markdown image link along with the alias:
e.g.
![MyImage](@images/path/to/image.png)
+